Compare commits
72 Commits
feat/featu
...
feat/inbox
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c4120f0a2 | ||
|
|
9f93af6134 | ||
|
|
68a5a9da1e | ||
|
|
1f8d5e45e1 | ||
|
|
8fd9730e2b | ||
|
|
04f6df6839 | ||
|
|
ba054ae915 | ||
|
|
1d1c6e5a55 | ||
|
|
bf71d2a14e | ||
|
|
163911255e | ||
|
|
24e38a3bbc | ||
|
|
dfd714f16a | ||
|
|
722081f89e | ||
|
|
f0e1df22b8 | ||
|
|
615cb263fb | ||
|
|
18faaf49d9 | ||
|
|
650b69ae56 | ||
|
|
eb4be963e3 | ||
|
|
27c27743e3 | ||
|
|
92930a2f63 | ||
|
|
7ad3365b0e | ||
|
|
f8bf4fea36 | ||
|
|
10cd8144eb | ||
|
|
66973a3745 | ||
|
|
85677bb792 | ||
|
|
7ae99d2038 | ||
|
|
70a5105783 | ||
|
|
420372ac9e | ||
|
|
6b00282a87 | ||
|
|
dae1001cbb | ||
|
|
af81d99b2a | ||
|
|
2751adc463 | ||
|
|
396ce9f3f3 | ||
|
|
3f4f66d878 | ||
|
|
d6751d7a26 | ||
|
|
0e32baff0b | ||
|
|
f76bf4c2c7 | ||
|
|
0d8532ab6d | ||
|
|
490d3d51e1 | ||
|
|
2ab796910e | ||
|
|
07102588be | ||
|
|
04f9422f24 | ||
|
|
05a7f5e178 | ||
|
|
7bae814f96 | ||
|
|
ea45e38fa0 | ||
|
|
6d9a85112f | ||
|
|
346efd19db | ||
|
|
617143a47f | ||
|
|
66f067276e | ||
|
|
fc10d0449f | ||
|
|
083f3e7108 | ||
|
|
af307a2a49 | ||
|
|
b063758ee5 | ||
|
|
4964b252e3 | ||
|
|
e468f5bbc9 | ||
|
|
c5b7b8a18a | ||
|
|
a8a1fbb829 | ||
|
|
3c2a4892e7 | ||
|
|
6d9e84d327 | ||
|
|
73b4e30c97 | ||
|
|
bd01545a70 | ||
|
|
6d360e581d | ||
|
|
2f2d5dfc0b | ||
|
|
0f27f4261b | ||
|
|
9b92cad2db | ||
|
|
ad1ff6159c | ||
|
|
034072f50e | ||
|
|
45d0d3f7e8 | ||
|
|
6e62eb8d81 | ||
|
|
3b9c57fe5c | ||
|
|
90e28cd3a4 | ||
|
|
e743e56787 |
@@ -12,6 +12,8 @@ NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|||||||
|
|
||||||
# [[DATABASE]]
|
# [[DATABASE]]
|
||||||
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
||||||
|
# Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool.
|
||||||
|
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
||||||
|
|
||||||
# [[SMTP]]
|
# [[SMTP]]
|
||||||
# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels
|
# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels
|
||||||
|
|||||||
32
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: 'github-actions'
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
target-branch: "feat/refresh"
|
||||||
|
labels:
|
||||||
|
- "ci dependencies"
|
||||||
|
- "ci"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/apps/marketing"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
target-branch: "feat/refresh"
|
||||||
|
labels:
|
||||||
|
- "npm dependencies"
|
||||||
|
- "frontend"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/apps/web"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
target-branch: "feat/refresh"
|
||||||
|
labels:
|
||||||
|
- "npm dependencies"
|
||||||
|
- "frontend"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
4
.github/workflows/ci.yml
vendored
@@ -6,6 +6,10 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "feat/refresh" ]
|
branches: [ "feat/refresh" ]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
|
|
||||||
|
|||||||
45
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [ feat/refresh ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ feat/refresh ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript' ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build Documenso
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
21
.github/workflows/semantic-pull-requests.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: "Validate PR Name"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-pr:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -89,6 +89,10 @@ Documenso is built using awesome open source tech including:
|
|||||||
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
|
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
|
||||||
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
|
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
|
||||||
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
|
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
|
||||||
|
- [Zod for schema declaration and validation](https://zod.dev/)
|
||||||
|
- [Lucide React for icons in React app](https://lucide.dev/)
|
||||||
|
- [Framer Motion for motion library](https://www.framer.com/motion/)
|
||||||
|
- [Radix UI for component library](https://www.radix-ui.com/)
|
||||||
- Check out `/package.json` and `/apps/web/package.json` for more
|
- Check out `/package.json` and `/apps/web/package.json` for more
|
||||||
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
|
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
|
||||||
|
|
||||||
|
|||||||
56
apps/marketing/content/blog/next.mdx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
title: Preview the next Documenso
|
||||||
|
description: We're redesigning Documenso by making it more elegant and appropriately playful. Here's a sneak peek.
|
||||||
|
authorName: 'Timur Ercan'
|
||||||
|
authorImage: '/blog/blog-author-timur.jpeg'
|
||||||
|
authorRole: 'Co-Founder'
|
||||||
|
date: 2023-08-21
|
||||||
|
tags:
|
||||||
|
- Design
|
||||||
|
- Preview
|
||||||
|
---
|
||||||
|
|
||||||
|
Since we launched [Documenso 0.9 on Product Hunt](https://producthunt.com/products/documenso#documenso) last May, the team's been hard at work behind the scenes to ramp up development and design to deliver an excellent next version.
|
||||||
|
|
||||||
|
Last week, Lucas shared the reasoning how [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite).
|
||||||
|
|
||||||
|
Today, I'm pleased to share with you a preview of the next Documenso.
|
||||||
|
|
||||||
|
## Preview the next Documenso
|
||||||
|
|
||||||
|
We redesigned the whole signing flow to make it more appealing and more convenient.
|
||||||
|
|
||||||
|
We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it.
|
||||||
|
|
||||||
|
**We call it happy minimalism.**
|
||||||
|
|
||||||
|
We paid particular attention to the moment of signing, which should be celebrated.
|
||||||
|
|
||||||
|
The image below is the final bloom of the completion celebration we added:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<MdxNextImage
|
||||||
|
src="/blog/blog-fig-preview-documenso.webp"
|
||||||
|
width="2000"
|
||||||
|
height="1268"
|
||||||
|
alt="Figure 1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<figcaption className="text-center">"You've signed a new document."</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## Kicking off a new phase of collaboration
|
||||||
|
|
||||||
|
This preview also is the kickoff for a new phase of how we collaborate with the community.
|
||||||
|
|
||||||
|
We recently [switched to Discord](https://documenso.com/blog/switching-from-slack-to-discord) to set up a more developer-friendly, community-driven environment, and we just released the [public roadmap](https://documen.so/launches).
|
||||||
|
|
||||||
|
As always, if you have any questions or feedback, please reach out. We love to hear from you.
|
||||||
|
|
||||||
|
Best from Hamburg,
|
||||||
|
|
||||||
|
Timur
|
||||||
|
|
||||||
|
Make sure to [star the GitHub repository](https://documen.so/github), [follow us on X](http://documen.so/twitter) and [join the Discord server](https://documen.so/discord) to keep up to date with all things Documenso.
|
||||||
|
|
||||||
|
We're building a beautiful, open-source alternative to DocuSign.
|
||||||
@@ -4,10 +4,11 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "PORT=3001 next dev",
|
"dev": "next dev -p 3001",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"recharts": "^2.7.2",
|
"recharts": "^2.7.2",
|
||||||
|
"sharp": "0.32.5",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
apps/marketing/public/blog/blog-fig-preview-documenso.webp
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
apps/marketing/public/logo_icon.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
56611
apps/marketing/public/pdf.worker.min.js
vendored
Normal file
@@ -8,7 +8,7 @@ import { FundingRaised } from './funding-raised';
|
|||||||
import { GithubMetric } from './gh-metrics';
|
import { GithubMetric } from './gh-metrics';
|
||||||
import { TeamMembers } from './team-members';
|
import { TeamMembers } from './team-members';
|
||||||
|
|
||||||
export const revalidate = 86400;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
const ZGithubStatsResponse = z.object({
|
const ZGithubStatsResponse = z.object({
|
||||||
stargazers_count: z.number(),
|
stargazers_count: z.number(),
|
||||||
|
|||||||
14
apps/marketing/src/app/robots.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
return {
|
||||||
|
rules: {
|
||||||
|
userAgent: '*',
|
||||||
|
allow: '/*',
|
||||||
|
disallow: ['/_next/*'],
|
||||||
|
},
|
||||||
|
sitemap: `${getBaseUrl()}/sitemap.xml`,
|
||||||
|
};
|
||||||
|
}
|
||||||
41
apps/marketing/src/app/sitemap.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
import { allBlogPosts, allGenericPages } from 'contentlayer/generated';
|
||||||
|
|
||||||
|
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
||||||
|
|
||||||
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
|
const baseUrl = getBaseUrl();
|
||||||
|
const lastModified = new Date();
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
url: baseUrl,
|
||||||
|
lastModified,
|
||||||
|
},
|
||||||
|
...allGenericPages.map((doc) => ({
|
||||||
|
url: `${baseUrl}/${doc._raw.flattenedPath}`,
|
||||||
|
lastModified,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/blog`,
|
||||||
|
lastModified,
|
||||||
|
},
|
||||||
|
...allBlogPosts.map((doc) => ({
|
||||||
|
url: `${baseUrl}/${doc._raw.flattenedPath}`,
|
||||||
|
lastModified,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/open`,
|
||||||
|
lastModified,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/oss-friends`,
|
||||||
|
lastModified,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/pricing`,
|
||||||
|
lastModified,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import React, { useState } from 'react';
|
|||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Info, Loader } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
import { usePlausible } from 'next-plausible';
|
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';
|
||||||
@@ -85,7 +85,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -97,50 +97,49 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
className={cn('flex flex-col gap-y-4', className)}
|
<fieldset disabled={isSubmitting} className={cn('flex flex-col gap-y-4', className)}>
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
{params?.get('cancelled') === 'true' && (
|
||||||
>
|
<div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4">
|
||||||
{params?.get('cancelled') === 'true' && (
|
<div className="flex">
|
||||||
<div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4">
|
<div className="flex-shrink-0">
|
||||||
<div className="flex">
|
<Info className="h-5 w-5 text-yellow-400" />
|
||||||
<div className="flex-shrink-0">
|
</div>
|
||||||
<Info className="h-5 w-5 text-yellow-400" />
|
<div className="ml-3">
|
||||||
</div>
|
<p className="text-sm leading-5 text-yellow-700">
|
||||||
<div className="ml-3">
|
You have cancelled the payment process. If you didn't mean to do this, please
|
||||||
<p className="text-sm leading-5 text-yellow-700">
|
try again.
|
||||||
You have cancelled the payment process. If you didn't mean to do this, please
|
</p>
|
||||||
try again.
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-500">Name</Label>
|
||||||
|
|
||||||
|
<Input type="text" className="mt-2" {...register('name')} autoFocus />
|
||||||
|
|
||||||
|
<FormErrorMessage className="mt-1" error={errors.name} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-500">Name</Label>
|
<Label className="text-slate-500">Email</Label>
|
||||||
|
|
||||||
<Input type="text" className="mt-2" {...register('name')} autoFocus />
|
<Input type="email" className="mt-2" {...register('email')} />
|
||||||
|
|
||||||
<FormErrorMessage className="mt-1" error={errors.name} />
|
<FormErrorMessage className="mt-1" error={errors.email} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<Button type="submit" size="lg" loading={isSubmitting}>
|
||||||
<Label className="text-slate-500">Email</Label>
|
Claim the Community Plan (
|
||||||
|
{/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
|
||||||
<Input type="email" className="mt-2" {...register('email')} />
|
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||||
|
? 'Monthly'
|
||||||
<FormErrorMessage className="mt-1" error={errors.email} />
|
: 'Yearly'}
|
||||||
</div>
|
)
|
||||||
|
</Button>
|
||||||
<Button type="submit" size="lg" disabled={isSubmitting}>
|
</fieldset>
|
||||||
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
|
|
||||||
Claim the Community Plan ({/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
|
|
||||||
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
|
||||||
? 'Monthly'
|
|
||||||
: 'Yearly'}
|
|
||||||
)
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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, Slack, Twitter } from 'lucide-react';
|
import { Github, MessagesSquare, Twitter } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
@@ -36,11 +36,11 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="https://documenso.slack.com"
|
href="https://documen.so/discord"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="hover:text-[#6D6D6D]"
|
className="hover:text-[#6D6D6D]"
|
||||||
>
|
>
|
||||||
<Slack className="h-6 w-6" />
|
<MessagesSquare className="h-6 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,6 +61,14 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
|||||||
Open
|
Open
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://shop.documenso.com"
|
||||||
|
target="_blank"
|
||||||
|
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
|
>
|
||||||
|
Shop
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="https://status.documenso.com"
|
href="https://status.documenso.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
'use client';
|
||||||
|
|
||||||
|
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 { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
import { HamburgerMenu } from './mobile-hamburger';
|
||||||
|
import { MobileNavigation } from './mobile-navigation';
|
||||||
|
|
||||||
export type HeaderProps = HTMLAttributes<HTMLElement>;
|
export type HeaderProps = HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
export const Header = ({ className, ...props }: HeaderProps) => {
|
export const Header = ({ className, ...props }: HeaderProps) => {
|
||||||
|
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={cn('flex items-center justify-between', className)} {...props}>
|
<header className={cn('flex items-center justify-between', className)} {...props}>
|
||||||
<Link href="/">
|
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
|
||||||
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
|
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center gap-x-6">
|
<div className="hidden items-center gap-x-6 md:flex">
|
||||||
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
|
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
|
||||||
Pricing
|
Pricing
|
||||||
</Link>
|
</Link>
|
||||||
@@ -35,6 +42,15 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
|||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<HamburgerMenu
|
||||||
|
onToggleMenuOpen={() => setIsHamburgerMenuOpen((v) => !v)}
|
||||||
|
isMenuOpen={isHamburgerMenuOpen}
|
||||||
|
/>
|
||||||
|
<MobileNavigation
|
||||||
|
isMenuOpen={isHamburgerMenuOpen}
|
||||||
|
onMenuOpenChange={setIsHamburgerMenuOpen}
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Menu, X } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
export interface HamburgerMenuProps {
|
||||||
|
isMenuOpen: boolean;
|
||||||
|
onToggleMenuOpen?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HamburgerMenu = ({ isMenuOpen, onToggleMenuOpen }: HamburgerMenuProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex md:hidden">
|
||||||
|
<Button variant="outline" className="z-20 w-10 p-0" onClick={onToggleMenuOpen}>
|
||||||
|
{isMenuOpen ? <X /> : <Menu />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
121
apps/marketing/src/components/(marketing)/mobile-navigation.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { motion, useReducedMotion } from 'framer-motion';
|
||||||
|
import { Github, MessagesSquare, Twitter } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
||||||
|
|
||||||
|
export type MobileNavigationProps = {
|
||||||
|
isMenuOpen: boolean;
|
||||||
|
onMenuOpenChange?: (_value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MENU_NAVIGATION_LINKS = [
|
||||||
|
{
|
||||||
|
href: '/blog',
|
||||||
|
text: 'Blog',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/pricing',
|
||||||
|
text: 'Pricing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://status.documenso.com',
|
||||||
|
text: 'Status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'mailto:support@documenso.com',
|
||||||
|
text: 'Support',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/privacy',
|
||||||
|
text: 'Privacy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://app.documenso.com/login',
|
||||||
|
text: 'Sign in',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
||||||
|
const shouldReduceMotion = useReducedMotion();
|
||||||
|
|
||||||
|
const handleMenuItemClick = () => {
|
||||||
|
onMenuOpenChange?.(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
||||||
|
<SheetContent className="w-full max-w-[400px]">
|
||||||
|
<Link href="/" className="z-10" onClick={handleMenuItemClick}>
|
||||||
|
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="mt-12 flex w-full flex-col items-start gap-y-4"
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
transition={{
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{MENU_NAVIGATION_LINKS.map(({ href, text }) => (
|
||||||
|
<motion.div
|
||||||
|
key={href}
|
||||||
|
variants={{
|
||||||
|
initial: {
|
||||||
|
opacity: 0,
|
||||||
|
x: shouldReduceMotion ? 0 : 100,
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
opacity: 1,
|
||||||
|
x: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="text-2xl font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
|
href={href}
|
||||||
|
onClick={() => handleMenuItemClick()}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="mx-auto mt-8 flex w-full flex-wrap items-center gap-x-4 gap-y-4 ">
|
||||||
|
<Link
|
||||||
|
href="https://twitter.com/documenso"
|
||||||
|
target="_blank"
|
||||||
|
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
|
>
|
||||||
|
<Twitter className="h-6 w-6" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://github.com/documenso/documenso"
|
||||||
|
target="_blank"
|
||||||
|
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
|
>
|
||||||
|
<Github className="h-6 w-6" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://documen.so/discord"
|
||||||
|
target="_blank"
|
||||||
|
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
|
>
|
||||||
|
<MessagesSquare className="h-6 w-6" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -21,12 +21,12 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { claimPlan } from '~/api/claim-plan/fetcher';
|
import { claimPlan } from '~/api/claim-plan/fetcher';
|
||||||
|
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
import { SignaturePad } from '../signature-pad';
|
|
||||||
|
|
||||||
const ZWidgetFormSchema = z
|
const ZWidgetFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
HTMLAttributes,
|
|
||||||
MouseEvent,
|
|
||||||
PointerEvent,
|
|
||||||
TouchEvent,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { StrokeOptions, getStroke } from 'perfect-freehand';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
import { getSvgPathFromStroke } from './helper';
|
|
||||||
import { Point } from './point';
|
|
||||||
|
|
||||||
const DPI = 2;
|
|
||||||
|
|
||||||
export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
|
|
||||||
onChange?: (_signatureDataUrl: string | null) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SignaturePad = ({ className, onChange, ...props }: SignaturePadProps) => {
|
|
||||||
const $el = useRef<HTMLCanvasElement>(null);
|
|
||||||
|
|
||||||
const [isPressed, setIsPressed] = useState(false);
|
|
||||||
const [points, setPoints] = useState<Point[]>([]);
|
|
||||||
|
|
||||||
const perfectFreehandOptions = useMemo(() => {
|
|
||||||
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
|
|
||||||
|
|
||||||
return {
|
|
||||||
size,
|
|
||||||
thinning: 0.25,
|
|
||||||
streamline: 0.5,
|
|
||||||
smoothing: 0.5,
|
|
||||||
end: {
|
|
||||||
taper: size * 2,
|
|
||||||
},
|
|
||||||
} satisfies StrokeOptions;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onMouseDown = (event: MouseEvent | PointerEvent | TouchEvent) => {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsPressed(true);
|
|
||||||
|
|
||||||
const point = Point.fromEvent(event, DPI, $el.current);
|
|
||||||
|
|
||||||
const newPoints = [...points, point];
|
|
||||||
|
|
||||||
setPoints(newPoints);
|
|
||||||
|
|
||||||
if ($el.current) {
|
|
||||||
const ctx = $el.current.getContext('2d');
|
|
||||||
|
|
||||||
if (ctx) {
|
|
||||||
ctx.save();
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.imageSmoothingQuality = 'high';
|
|
||||||
|
|
||||||
const pathData = new Path2D(
|
|
||||||
getSvgPathFromStroke(getStroke(newPoints, perfectFreehandOptions)),
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.fill(pathData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseMove = (event: MouseEvent | PointerEvent | TouchEvent) => {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPressed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const point = Point.fromEvent(event, DPI, $el.current);
|
|
||||||
|
|
||||||
if (point.distanceTo(points[points.length - 1]) > 5) {
|
|
||||||
const newPoints = [...points, point];
|
|
||||||
|
|
||||||
setPoints(newPoints);
|
|
||||||
|
|
||||||
if ($el.current) {
|
|
||||||
const ctx = $el.current.getContext('2d');
|
|
||||||
|
|
||||||
if (ctx) {
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.imageSmoothingQuality = 'high';
|
|
||||||
|
|
||||||
const pathData = new Path2D(
|
|
||||||
getSvgPathFromStroke(getStroke(points, perfectFreehandOptions)),
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.fill(pathData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addPoint = true) => {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsPressed(false);
|
|
||||||
|
|
||||||
const point = Point.fromEvent(event, DPI, $el.current);
|
|
||||||
|
|
||||||
const newPoints = [...points];
|
|
||||||
|
|
||||||
if (addPoint) {
|
|
||||||
newPoints.push(point);
|
|
||||||
|
|
||||||
setPoints(newPoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($el.current) {
|
|
||||||
const ctx = $el.current.getContext('2d');
|
|
||||||
|
|
||||||
if (ctx) {
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.imageSmoothingQuality = 'high';
|
|
||||||
|
|
||||||
const pathData = new Path2D(
|
|
||||||
getSvgPathFromStroke(getStroke(newPoints, perfectFreehandOptions)),
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.fill(pathData);
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange?.($el.current.toDataURL());
|
|
||||||
}
|
|
||||||
|
|
||||||
setPoints([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseEnter = (event: MouseEvent | PointerEvent | TouchEvent) => {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('buttons' in event && event.buttons === 1) {
|
|
||||||
onMouseDown(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseLeave = (event: MouseEvent | PointerEvent | TouchEvent) => {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseUp(event, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClearClick = () => {
|
|
||||||
if ($el.current) {
|
|
||||||
const ctx = $el.current.getContext('2d');
|
|
||||||
|
|
||||||
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange?.(null);
|
|
||||||
|
|
||||||
setPoints([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ($el.current) {
|
|
||||||
$el.current.width = $el.current.clientWidth * DPI;
|
|
||||||
$el.current.height = $el.current.clientHeight * DPI;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative block">
|
|
||||||
<canvas
|
|
||||||
ref={$el}
|
|
||||||
className={cn('relative block', className)}
|
|
||||||
style={{ touchAction: 'none' }}
|
|
||||||
onPointerMove={(event) => onMouseMove(event)}
|
|
||||||
onPointerDown={(event) => onMouseDown(event)}
|
|
||||||
onPointerUp={(event) => onMouseUp(event)}
|
|
||||||
onPointerLeave={(event) => onMouseLeave(event)}
|
|
||||||
onPointerEnter={(event) => onMouseEnter(event)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="absolute bottom-2 right-2">
|
|
||||||
<button className="rounded-full p-2 text-xs text-slate-500" onClick={() => onClearClick()}>
|
|
||||||
Clear Signature
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
27
apps/marketing/src/hooks/use-window-size.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useWindowSize() {
|
||||||
|
const [size, setSize] = useState({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
setSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onResize();
|
||||||
|
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
@@ -9,16 +9,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": [
|
"~/*": ["./src/*"],
|
||||||
"./src/*"
|
"contentlayer/generated": ["./.contentlayer/generated"]
|
||||||
],
|
|
||||||
"contentlayer/generated": [
|
|
||||||
"./.contentlayer/generated"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"types": [
|
"types": ["@documenso/lib/types/next-auth.d.ts"],
|
||||||
"@documenso/lib/types/next-auth.d.ts"
|
|
||||||
],
|
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"incremental": false
|
"incremental": false
|
||||||
},
|
},
|
||||||
@@ -29,7 +23,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".contentlayer/generated"
|
".contentlayer/generated"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules"]
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "PORT=3000 next dev",
|
"dev": "next dev -p 3000",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.214.0",
|
"lucide-react": "^0.214.0",
|
||||||
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"next": "13.4.12",
|
"next": "13.4.12",
|
||||||
@@ -34,14 +36,15 @@
|
|||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-pdf": "^7.1.1",
|
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
|
"sharp": "0.32.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/formidable": "^2.0.6",
|
"@types/formidable": "^2.0.6",
|
||||||
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/node": "20.1.0",
|
"@types/node": "20.1.0",
|
||||||
"@types/react": "18.2.18",
|
"@types/react": "18.2.18",
|
||||||
"@types/react-dom": "18.2.7"
|
"@types/react-dom": "18.2.7"
|
||||||
|
|||||||
56611
apps/web/public/pdf.worker.min.js
vendored
Normal file
@@ -22,14 +22,14 @@ import { LocaleDate } from '~/components/formatter/locale-date';
|
|||||||
import { UploadDocument } from './upload-document';
|
import { UploadDocument } from './upload-document';
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const session = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const [stats, results] = await Promise.all([
|
const [stats, results] = await Promise.all([
|
||||||
getStats({
|
getStats({
|
||||||
userId: session.id,
|
user,
|
||||||
}),
|
}),
|
||||||
findDocuments({
|
findDocuments({
|
||||||
userId: session.id,
|
userId: user.id,
|
||||||
perPage: 10,
|
perPage: 10,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useCreateDocument } from '~/api/document/create/fetcher';
|
import { useCreateDocument } from '~/api/document/create/fetcher';
|
||||||
import { DocumentDropzone } from '~/components/(dashboard)/document-dropzone/document-dropzone';
|
|
||||||
|
|
||||||
export type UploadDocumentProps = {
|
export type UploadDocumentProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -2,14 +2,28 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Document, Field, Recipient, User } from '@documenso/prisma/client';
|
import { Document, Field, Recipient, User } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
|
||||||
|
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
||||||
|
import { AddSignersFormPartial } from '@documenso/ui/primitives/document-flow/add-signers';
|
||||||
|
import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
|
||||||
|
import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/add-subject';
|
||||||
|
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.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 { LazyPDFViewer } from '~/components/(dashboard)/pdf-viewer/lazy-pdf-viewer';
|
import { addFields } from '~/components/forms/edit-document/add-fields.action';
|
||||||
import { AddFieldsFormPartial } from '~/components/forms/edit-document/add-fields';
|
import { addSigners } from '~/components/forms/edit-document/add-signers.action';
|
||||||
import { AddSignersFormPartial } from '~/components/forms/edit-document/add-signers';
|
import { completeDocument } from '~/components/forms/edit-document/add-subject.action';
|
||||||
import { AddSubjectFormPartial } from '~/components/forms/edit-document/add-subject';
|
|
||||||
|
|
||||||
export type EditDocumentFormProps = {
|
export type EditDocumentFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -19,6 +33,8 @@ export type EditDocumentFormProps = {
|
|||||||
fields: Field[];
|
fields: Field[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EditDocumentStep = 'signers' | 'fields' | 'subject';
|
||||||
|
|
||||||
export const EditDocumentForm = ({
|
export const EditDocumentForm = ({
|
||||||
className,
|
className,
|
||||||
document,
|
document,
|
||||||
@@ -26,27 +42,103 @@ export const EditDocumentForm = ({
|
|||||||
fields,
|
fields,
|
||||||
user: _user,
|
user: _user,
|
||||||
}: EditDocumentFormProps) => {
|
}: EditDocumentFormProps) => {
|
||||||
const [step, setStep] = useState<'signers' | 'fields' | 'subject'>('signers');
|
const { toast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [step, setStep] = useState<EditDocumentStep>('signers');
|
||||||
|
|
||||||
const documentUrl = `data:application/pdf;base64,${document.document}`;
|
const documentUrl = `data:application/pdf;base64,${document.document}`;
|
||||||
|
|
||||||
const onNextStep = () => {
|
const documentFlow: Record<EditDocumentStep, DocumentFlowStep> = {
|
||||||
if (step === 'signers') {
|
signers: {
|
||||||
setStep('fields');
|
title: 'Add Signers',
|
||||||
}
|
description: 'Add the people who will sign the document.',
|
||||||
|
stepIndex: 1,
|
||||||
|
onSubmit: () => onAddSignersFormSubmit,
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
title: 'Add Fields',
|
||||||
|
description: 'Add all relevant fields for each recipient.',
|
||||||
|
stepIndex: 2,
|
||||||
|
onBackStep: () => setStep('signers'),
|
||||||
|
onSubmit: () => onAddFieldsFormSubmit,
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
title: 'Add Subject',
|
||||||
|
description: 'Add the subject and message you wish to send to signers.',
|
||||||
|
stepIndex: 3,
|
||||||
|
onBackStep: () => setStep('fields'),
|
||||||
|
onSubmit: () => onAddSubjectFormSubmit,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (step === 'fields') {
|
const currentDocumentFlow = documentFlow[step];
|
||||||
setStep('subject');
|
|
||||||
|
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
||||||
|
try {
|
||||||
|
// Custom invocation server action
|
||||||
|
await addSigners({
|
||||||
|
documentId: document.id,
|
||||||
|
signers: data.signers,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
|
|
||||||
|
setStep('fields');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'An error occurred while adding signers.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPreviousStep = () => {
|
const onAddFieldsFormSubmit = async (data: TAddFieldsFormSchema) => {
|
||||||
if (step === 'fields') {
|
try {
|
||||||
setStep('signers');
|
// Custom invocation server action
|
||||||
}
|
await addFields({
|
||||||
|
documentId: document.id,
|
||||||
|
fields: data.fields,
|
||||||
|
});
|
||||||
|
|
||||||
if (step === 'subject') {
|
router.refresh();
|
||||||
setStep('fields');
|
|
||||||
|
setStep('subject');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'An error occurred while adding signers.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
||||||
|
const { subject, message } = data.email;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await completeDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
email: {
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'An error occurred while sending the document.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,35 +154,43 @@ export const EditDocumentForm = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||||
{step === 'signers' && (
|
<DocumentFlowFormContainer onSubmit={(e) => e.preventDefault()}>
|
||||||
<AddSignersFormPartial
|
<DocumentFlowFormContainerHeader
|
||||||
recipients={recipients}
|
title={currentDocumentFlow.title}
|
||||||
fields={fields}
|
description={currentDocumentFlow.description}
|
||||||
document={document}
|
|
||||||
onContinue={onNextStep}
|
|
||||||
onGoBack={onPreviousStep}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'fields' && (
|
{step === 'signers' && (
|
||||||
<AddFieldsFormPartial
|
<AddSignersFormPartial
|
||||||
recipients={recipients}
|
documentFlow={documentFlow.signers}
|
||||||
fields={fields}
|
recipients={recipients}
|
||||||
document={document}
|
fields={fields}
|
||||||
onContinue={onNextStep}
|
numberOfSteps={Object.keys(documentFlow).length}
|
||||||
onGoBack={onPreviousStep}
|
onSubmit={onAddSignersFormSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{step === 'subject' && (
|
{step === 'fields' && (
|
||||||
<AddSubjectFormPartial
|
<AddFieldsFormPartial
|
||||||
recipients={recipients}
|
documentFlow={documentFlow.fields}
|
||||||
fields={fields}
|
recipients={recipients}
|
||||||
document={document}
|
fields={fields}
|
||||||
onContinue={onNextStep}
|
numberOfSteps={Object.keys(documentFlow).length}
|
||||||
onGoBack={onPreviousStep}
|
onSubmit={onAddFieldsFormSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{step === 'subject' && (
|
||||||
|
<AddSubjectFormPartial
|
||||||
|
documentFlow={documentFlow.subject}
|
||||||
|
document={document}
|
||||||
|
recipients={recipients}
|
||||||
|
fields={fields}
|
||||||
|
numberOfSteps={Object.keys(documentFlow).length}
|
||||||
|
onSubmit={onAddSubjectFormSubmit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DocumentFlowFormContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,34 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
import { PDFViewerProps } from '~/components/(dashboard)/pdf-viewer/pdf-viewer';
|
import { PDFViewerProps } from '@documenso/ui/primitives/pdf-viewer';
|
||||||
|
|
||||||
export type LoadablePDFCard = PDFViewerProps & {
|
export type LoadablePDFCard = PDFViewerProps & {
|
||||||
className?: string;
|
className?: string;
|
||||||
pdfClassName?: string;
|
pdfClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PDFViewer = dynamic(async () => import('~/components/(dashboard)/pdf-viewer/pdf-viewer'), {
|
|
||||||
ssr: false,
|
|
||||||
loading: () => (
|
|
||||||
<div className="flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
|
|
||||||
<Loader className="h-12 w-12 animate-spin text-slate-500" />
|
|
||||||
|
|
||||||
<p className="mt-4 text-slate-500">Loading document...</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LoadablePDFCard = ({ className, pdfClassName, ...props }: LoadablePDFCard) => {
|
export const LoadablePDFCard = ({ className, pdfClassName, ...props }: LoadablePDFCard) => {
|
||||||
return (
|
return (
|
||||||
<Card className={className} gradient {...props}>
|
<Card className={className} gradient {...props}>
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-2">
|
||||||
<PDFViewer className={pdfClassName} {...props} />
|
<LazyPDFViewer className={pdfClassName} {...props} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ export default function Loading() {
|
|||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
Documents
|
Documents
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||||
Loading Document...
|
Loading Document...
|
||||||
</h1>
|
</h1>
|
||||||
<div className="mt-8 grid min-h-[80vh] w-full grid-cols-12 gap-x-8">
|
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
|
||||||
<div className="dark:bg-background border-border col-span-12 rounded-xl border-2 bg-white/50 p-2 before:rounded-xl lg:col-span-6 xl:col-span-7">
|
<div className="dark:bg-background border-border col-span-12 rounded-xl border-2 bg-white/50 p-2 before:rounded-xl lg:col-span-6 xl:col-span-7">
|
||||||
<div className="flex min-h-[80vh] flex-col items-center justify-center">
|
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center">
|
||||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">Loading document...</p>
|
<p className="text-muted-foreground mt-4">Loading document...</p>
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
|||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
|
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
||||||
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
|
|
||||||
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
|
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
|
||||||
|
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
|
|
||||||
export type DocumentPageProps = {
|
export type DocumentPageProps = {
|
||||||
@@ -69,18 +72,28 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
<div className="text-muted-foreground flex items-center">
|
<div className="text-muted-foreground flex items-center">
|
||||||
<Users2 className="mr-2 h-5 w-5" />
|
<Users2 className="mr-2 h-5 w-5" />
|
||||||
|
|
||||||
<span>{recipients.length} Recipient(s)</span>
|
<StackAvatarsWithTooltip recipients={recipients} position="bottom">
|
||||||
|
<span>{recipients.length} Recipient(s)</span>
|
||||||
|
</StackAvatarsWithTooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditDocumentForm
|
{document.status !== InternalDocumentStatus.COMPLETED && (
|
||||||
className="mt-8"
|
<EditDocumentForm
|
||||||
document={document}
|
className="mt-8"
|
||||||
user={session}
|
document={document}
|
||||||
recipients={recipients}
|
user={session}
|
||||||
fields={fields}
|
recipients={recipients}
|
||||||
/>
|
fields={fields}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{document.status === InternalDocumentStatus.COMPLETED && (
|
||||||
|
<div className="mx-auto mt-12 max-w-2xl">
|
||||||
|
<LazyPDFViewer document={`data:application/pdf;base64,${document.document}`} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { Edit, Pencil, Share } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
export type DataTableActionButtonProps = {
|
||||||
|
row: Document & {
|
||||||
|
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
|
Recipient: Recipient[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
|
||||||
|
|
||||||
|
const isOwner = row.User.id === session.user.id;
|
||||||
|
const isRecipient = !!recipient;
|
||||||
|
const isDraft = row.status === DocumentStatus.DRAFT;
|
||||||
|
const isPending = row.status === DocumentStatus.PENDING;
|
||||||
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
|
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
|
|
||||||
|
return match({
|
||||||
|
isOwner,
|
||||||
|
isRecipient,
|
||||||
|
isDraft,
|
||||||
|
isPending,
|
||||||
|
isComplete,
|
||||||
|
isSigned,
|
||||||
|
})
|
||||||
|
.with({ isOwner: true, isDraft: true }, () => (
|
||||||
|
<Button className="w-24" asChild>
|
||||||
|
<Link href={`/documents/${row.id}`}>
|
||||||
|
<Edit className="-ml-1 mr-2 h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
|
||||||
|
<Button className="w-24" asChild>
|
||||||
|
<Link href={`/sign/${recipient?.token}`}>
|
||||||
|
<Pencil className="-ml-1 mr-2 h-4 w-4" />
|
||||||
|
Sign
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
.otherwise(() => (
|
||||||
|
<Button className="w-24" disabled>
|
||||||
|
<Share className="-ml-1 mr-2 h-4 w-4" />
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
));
|
||||||
|
};
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
Download,
|
||||||
|
Edit,
|
||||||
|
History,
|
||||||
|
MoreHorizontal,
|
||||||
|
Pencil,
|
||||||
|
Share,
|
||||||
|
Trash2,
|
||||||
|
XCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
|
import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
|
|
||||||
|
export type DataTableActionDropdownProps = {
|
||||||
|
row: Document & {
|
||||||
|
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
|
Recipient: Recipient[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
|
||||||
|
|
||||||
|
const isOwner = row.User.id === session.user.id;
|
||||||
|
// const isRecipient = !!recipient;
|
||||||
|
// const isDraft = row.status === DocumentStatus.DRAFT;
|
||||||
|
// const isPending = row.status === DocumentStatus.PENDING;
|
||||||
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
|
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
|
|
||||||
|
const onDownloadClick = () => {
|
||||||
|
let decodedDocument = row.document;
|
||||||
|
|
||||||
|
try {
|
||||||
|
decodedDocument = atob(decodedDocument);
|
||||||
|
} catch (err) {
|
||||||
|
// We're just going to ignore this error and try to download the document
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentBytes = Uint8Array.from(decodedDocument.split('').map((c) => c.charCodeAt(0)));
|
||||||
|
|
||||||
|
const blob = new Blob([documentBytes], {
|
||||||
|
type: 'application/pdf',
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = window.document.createElement('a');
|
||||||
|
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = row.title || 'document.pdf';
|
||||||
|
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
window.URL.revokeObjectURL(link.href);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<MoreHorizontal className="h-5 w-5 text-gray-500" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
|
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||||
|
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled={!recipient} asChild>
|
||||||
|
<Link href={`/sign/${recipient?.token}`}>
|
||||||
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
|
Sign
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled={!isOwner} asChild>
|
||||||
|
<Link href={`/documents/${row.id}`}>
|
||||||
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled={!isComplete} onClick={onDownloadClick}>
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<XCircle className="mr-2 h-4 w-4" />
|
||||||
|
Void
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuLabel>Share</DropdownMenuLabel>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<History className="mr-2 h-4 w-4" />
|
||||||
|
Resend
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Share className="mr-2 h-4 w-4" />
|
||||||
|
Share
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ import { Loader } from 'lucide-react';
|
|||||||
|
|
||||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||||
import { FindResultSet } from '@documenso/lib/types/find-result-set';
|
import { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||||
import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient';
|
import { Document, Recipient, User } from '@documenso/prisma/client';
|
||||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||||
|
|
||||||
@@ -16,8 +16,16 @@ import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-a
|
|||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||||
|
|
||||||
|
import { DataTableActionButton } from './data-table-action-button';
|
||||||
|
import { DataTableActionDropdown } from './data-table-action-dropdown';
|
||||||
|
|
||||||
export type DocumentsDataTableProps = {
|
export type DocumentsDataTableProps = {
|
||||||
results: FindResultSet<DocumentWithReciepient>;
|
results: FindResultSet<
|
||||||
|
Document & {
|
||||||
|
Recipient: Recipient[];
|
||||||
|
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
||||||
@@ -45,7 +53,11 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
|||||||
{
|
{
|
||||||
header: 'Title',
|
header: 'Title',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Link href={`/documents/${row.original.id}`} className="font-medium hover:underline">
|
<Link
|
||||||
|
href={`/documents/${row.original.id}`}
|
||||||
|
title={row.original.title}
|
||||||
|
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
|
||||||
|
>
|
||||||
{row.original.title}
|
{row.original.title}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
@@ -67,6 +79,15 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
|||||||
accessorKey: 'created',
|
accessorKey: 'created',
|
||||||
cell: ({ row }) => <LocaleDate date={row.getValue('created')} />,
|
cell: ({ row }) => <LocaleDate date={row.getValue('created')} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'Actions',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-x-4">
|
||||||
|
<DataTableActionButton row={row.original} />
|
||||||
|
<DataTableActionDropdown row={row.original} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
data={results.data}
|
data={results.data}
|
||||||
perPage={results.perPage}
|
perPage={results.perPage}
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
||||||
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
||||||
import { isDocumentStatus } from '@documenso/lib/types/is-document-status';
|
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
import { DocumentDropzone } from '~/components/(dashboard)/document-dropzone/document-dropzone';
|
|
||||||
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
||||||
import {
|
import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
|
||||||
PeriodSelectorValue,
|
|
||||||
isPeriodSelectorValue,
|
|
||||||
} from '~/components/(dashboard)/period-selector/types';
|
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
|
|
||||||
|
import { UploadDocument } from '../dashboard/upload-document';
|
||||||
import { DocumentsDataTable } from './data-table';
|
import { DocumentsDataTable } from './data-table';
|
||||||
|
|
||||||
export type DocumentsPageProps = {
|
export type DocumentsPageProps = {
|
||||||
searchParams?: {
|
searchParams?: {
|
||||||
status?: InternalDocumentStatus | 'ALL';
|
status?: ExtendedDocumentStatus;
|
||||||
period?: PeriodSelectorValue;
|
period?: PeriodSelectorValue;
|
||||||
page?: string;
|
page?: string;
|
||||||
perPage?: string;
|
perPage?: string;
|
||||||
@@ -30,20 +24,20 @@ export type DocumentsPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||||
const session = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const stats = await getStats({
|
const stats = await getStats({
|
||||||
userId: session.id,
|
user,
|
||||||
});
|
});
|
||||||
|
|
||||||
const status = isDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
|
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
|
||||||
const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
|
// const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
|
||||||
const page = Number(searchParams.page) || 1;
|
const page = Number(searchParams.page) || 1;
|
||||||
const perPage = Number(searchParams.perPage) || 20;
|
const perPage = Number(searchParams.perPage) || 20;
|
||||||
|
|
||||||
const results = await findDocuments({
|
const results = await findDocuments({
|
||||||
userId: session.id,
|
userId: user.id,
|
||||||
status: status === 'ALL' ? undefined : status,
|
status,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
column: 'created',
|
column: 'created',
|
||||||
direction: 'desc',
|
direction: 'desc',
|
||||||
@@ -52,8 +46,6 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
perPage,
|
perPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isNoResults = status === 'ALL' && period === '' && results.data.length === 0;
|
|
||||||
|
|
||||||
const getTabHref = (value: typeof status) => {
|
const getTabHref = (value: typeof status) => {
|
||||||
const params = new URLSearchParams(searchParams);
|
const params = new URLSearchParams(searchParams);
|
||||||
|
|
||||||
@@ -63,73 +55,47 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
params.delete('page');
|
params.delete('page');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === 'ALL') {
|
|
||||||
params.delete('status');
|
|
||||||
}
|
|
||||||
|
|
||||||
return `/documents?${params.toString()}`;
|
return `/documents?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
||||||
<h1 className="text-4xl font-semibold">All Documents</h1>
|
<UploadDocument />
|
||||||
|
|
||||||
|
<h1 className="mt-12 text-4xl font-semibold">Documents</h1>
|
||||||
|
|
||||||
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
|
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
|
||||||
<Tabs defaultValue={status}>
|
<Tabs defaultValue={status} className="overflow-x-auto">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger className="min-w-[60px]" value="ALL" asChild>
|
{[
|
||||||
<Link href={getTabHref('ALL')}>All</Link>
|
ExtendedDocumentStatus.INBOX,
|
||||||
</TabsTrigger>
|
ExtendedDocumentStatus.PENDING,
|
||||||
|
ExtendedDocumentStatus.COMPLETED,
|
||||||
|
ExtendedDocumentStatus.DRAFT,
|
||||||
|
ExtendedDocumentStatus.ALL,
|
||||||
|
].map((value) => (
|
||||||
|
<TabsTrigger key={value} className="min-w-[60px]" value={value} asChild>
|
||||||
|
<Link href={getTabHref(value)} scroll={false}>
|
||||||
|
<DocumentStatus status={value} />
|
||||||
|
|
||||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.DRAFT} asChild>
|
{value !== ExtendedDocumentStatus.ALL && (
|
||||||
<Link href={getTabHref(InternalDocumentStatus.DRAFT)}>
|
<span className="ml-1 hidden opacity-50 md:inline-block">
|
||||||
<DocumentStatus status={InternalDocumentStatus.DRAFT} />
|
{Math.min(stats[value], 99)}
|
||||||
|
</span>
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
)}
|
||||||
{Math.min(stats.DRAFT, 99)}
|
</Link>
|
||||||
</span>
|
</TabsTrigger>
|
||||||
</Link>
|
))}
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.PENDING} asChild>
|
|
||||||
<Link href={getTabHref(InternalDocumentStatus.PENDING)}>
|
|
||||||
<DocumentStatus status={InternalDocumentStatus.PENDING} />
|
|
||||||
|
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
|
||||||
{Math.min(stats.PENDING, 99)}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.COMPLETED} asChild>
|
|
||||||
<Link href={getTabHref(InternalDocumentStatus.COMPLETED)}>
|
|
||||||
<DocumentStatus status={InternalDocumentStatus.COMPLETED} />
|
|
||||||
|
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
|
||||||
{Math.min(stats.COMPLETED, 99)}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="flex flex-1 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
<div className="flex flex-1 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
||||||
<PeriodSelector />
|
<PeriodSelector />
|
||||||
|
|
||||||
<Button>
|
|
||||||
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
|
||||||
Add Document
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
{/* If we're viewing all documents for all time and there's nuffin we should should an add document component instead */}
|
<DocumentsDataTable results={results} />
|
||||||
{isNoResults ? (
|
|
||||||
<DocumentDropzone className="min-h-[60vh] md:min-h-[40vh]" />
|
|
||||||
) : (
|
|
||||||
<DocumentsDataTable results={results} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { SignaturePad } from '~/components/signature-pad';
|
|
||||||
|
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { useRequiredSigningContext } from './provider';
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className={cn(
|
className={cn(
|
||||||
'dark:bg-background border-border bg-widget sticky top-20 flex h-[calc(100vh-6rem)] max-h-screen flex-col rounded-xl border px-4 py-6',
|
'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[80rem] flex-col rounded-xl border px-4 py-6',
|
||||||
)}
|
)}
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
>
|
>
|
||||||
@@ -65,7 +64,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
|
|
||||||
<hr className="border-border mb-8 mt-4" />
|
<hr className="border-border mb-8 mt-4" />
|
||||||
|
|
||||||
<div className="-mx-2 flex flex-1 flex-col overflow-y-auto px-2">
|
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="full-name">Full Name</Label>
|
<Label htmlFor="full-name">Full Name</Label>
|
||||||
@@ -99,10 +98,10 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex flex-col gap-4 md:flex-row">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 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"
|
||||||
>
|
>
|
||||||
@@ -110,8 +109,8 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
className="w-full"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex-1"
|
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={!isComplete || isSubmitting}
|
disabled={!isComplete || isSubmitting}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
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 { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
@@ -9,9 +10,7 @@ import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-re
|
|||||||
import { FieldType } from '@documenso/prisma/client';
|
import { FieldType } from '@documenso/prisma/client';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
import { LazyPDFViewer } from '~/components/(dashboard)/pdf-viewer/lazy-pdf-viewer';
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
|
|
||||||
|
|
||||||
import { DateField } from './date-field';
|
import { DateField } from './date-field';
|
||||||
import { SigningForm } from './form';
|
import { SigningForm } from './form';
|
||||||
@@ -58,7 +57,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 grid grid-cols-12 gap-8">
|
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">
|
||||||
<Card
|
<Card
|
||||||
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
|
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
|
||||||
gradient
|
gradient
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import { trpc } from '@documenso/trpc/react';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
|
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { SignaturePad } from '~/components/signature-pad';
|
|
||||||
|
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { useRequiredSigningContext } from './provider';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { SigningFieldContainer } from './signing-field-container';
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 394 KiB After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 20 MiB After Width: | Height: | Size: 14 MiB |
@@ -46,7 +46,7 @@ export const StackAvatar = ({ first, zIndex, fallbackText, type }: StackAvatarPr
|
|||||||
className={`
|
className={`
|
||||||
${zIndexClass}
|
${zIndexClass}
|
||||||
${firstClass}
|
${firstClass}
|
||||||
h-10 w-10 border-2 border-solid border-white`}
|
dark:border-border h-10 w-10 border-2 border-solid border-white`}
|
||||||
>
|
>
|
||||||
<AvatarFallback className={classes}>{fallbackText ?? 'UK'}</AvatarFallback>
|
<AvatarFallback className={classes}>{fallbackText ?? 'UK'}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|||||||
@@ -11,7 +11,17 @@ import {
|
|||||||
import { StackAvatar } from './stack-avatar';
|
import { StackAvatar } from './stack-avatar';
|
||||||
import { StackAvatars } from './stack-avatars';
|
import { StackAvatars } from './stack-avatars';
|
||||||
|
|
||||||
export const StackAvatarsWithTooltip = ({ recipients }: { recipients: Recipient[] }) => {
|
export type StackAvatarsWithTooltipProps = {
|
||||||
|
recipients: Recipient[];
|
||||||
|
position?: 'top' | 'bottom';
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StackAvatarsWithTooltip = ({
|
||||||
|
recipients,
|
||||||
|
position,
|
||||||
|
children,
|
||||||
|
}: StackAvatarsWithTooltipProps) => {
|
||||||
const waitingRecipients = recipients.filter(
|
const waitingRecipients = recipients.filter(
|
||||||
(recipient) => getRecipientType(recipient) === 'waiting',
|
(recipient) => getRecipientType(recipient) === 'waiting',
|
||||||
);
|
);
|
||||||
@@ -32,9 +42,10 @@ export const StackAvatarsWithTooltip = ({ recipients }: { recipients: Recipient[
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="flex cursor-pointer">
|
<TooltipTrigger className="flex cursor-pointer">
|
||||||
<StackAvatars recipients={recipients} />
|
{children || <StackAvatars recipients={recipients} />}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
|
||||||
|
<TooltipContent side={position}>
|
||||||
<div className="flex flex-col gap-y-5 p-1">
|
<div className="flex flex-col gap-y-5 p-1">
|
||||||
{completedRecipients.length > 0 && (
|
{completedRecipients.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -2,30 +2,17 @@
|
|||||||
|
|
||||||
import { HTMLAttributes } from 'react';
|
import { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||||
const pathname = usePathname();
|
// const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('ml-8 hidden flex-1 gap-x-6 md:flex', className)} {...props}>
|
<div className={cn('ml-8 hidden flex-1 gap-x-6 md:flex', className)} {...props}>
|
||||||
<Link
|
{/* We have no other subpaths rn */}
|
||||||
href="/dashboard"
|
{/* <Link
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
|
||||||
{
|
|
||||||
'text-foreground': pathname?.startsWith('/dashboard'),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/documents"
|
href="/documents"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 ',
|
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 ',
|
||||||
@@ -35,14 +22,6 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Documents
|
Documents
|
||||||
</Link>
|
|
||||||
{/* <Link
|
|
||||||
href="/settings/profile"
|
|
||||||
className={cn('font-medium leading-5 text-[#A1A1AA] hover:opacity-80', {
|
|
||||||
'text-primary-foreground': pathname?.startsWith('/settings'),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</Link> */}
|
</Link> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ import { HTMLAttributes } from 'react';
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Menu } from 'lucide-react';
|
|
||||||
|
|
||||||
import { User } from '@documenso/prisma/client';
|
import { User } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { Logo } from '~/components/branding/logo';
|
||||||
|
|
||||||
@@ -23,7 +20,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
|
|||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-40 flex h-16 w-full items-center border-b backdrop-blur',
|
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-50 flex h-16 w-full items-center border-b backdrop-blur',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -41,9 +38,9 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
|
|||||||
<div className="flex gap-x-4">
|
<div className="flex gap-x-4">
|
||||||
<ProfileDropdown user={user} />
|
<ProfileDropdown user={user} />
|
||||||
|
|
||||||
<Button variant="outline" size="sm" className="h-10 w-10 p-0.5 md:hidden">
|
{/* <Button variant="outline" size="sm" className="h-10 w-10 p-0.5 md:hidden">
|
||||||
<Menu className="h-6 w-6" />
|
<Menu className="h-6 w-6" />
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
|
||||||
|
|
||||||
export const LazyPDFViewer = dynamic(
|
|
||||||
async () => import('~/components/(dashboard)/pdf-viewer/pdf-viewer'),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
loading: () => (
|
|
||||||
<div className="dark:bg-background flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
|
|
||||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">Loading document...</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -39,7 +39,7 @@ export const PeriodSelector = () => {
|
|||||||
params.delete('period');
|
params.delete('period');
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push(`${pathname}?${params.toString()}`);
|
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { claimPlan } from '~/api/claim-plan/fetcher';
|
import { claimPlan } from '~/api/claim-plan/fetcher';
|
||||||
|
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
import { SignaturePad } from '../signature-pad';
|
|
||||||
|
|
||||||
const ZWidgetFormSchema = z
|
const ZWidgetFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -3,21 +3,17 @@ import { HTMLAttributes } from 'react';
|
|||||||
import { CheckCircle2, Clock, File } from 'lucide-react';
|
import { CheckCircle2, Clock, File } from 'lucide-react';
|
||||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||||
|
|
||||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
type FriendlyStatus = {
|
type FriendlyStatus = {
|
||||||
label: string;
|
label: string;
|
||||||
icon: LucideIcon;
|
icon?: LucideIcon;
|
||||||
color: string;
|
color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus> = {
|
||||||
DRAFT: {
|
|
||||||
label: 'Draft',
|
|
||||||
icon: File,
|
|
||||||
color: 'text-yellow-500',
|
|
||||||
},
|
|
||||||
PENDING: {
|
PENDING: {
|
||||||
label: 'Pending',
|
label: 'Pending',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
@@ -28,10 +24,24 @@ const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
|||||||
icon: CheckCircle2,
|
icon: CheckCircle2,
|
||||||
color: 'text-green-500',
|
color: 'text-green-500',
|
||||||
},
|
},
|
||||||
|
DRAFT: {
|
||||||
|
label: 'Draft',
|
||||||
|
icon: File,
|
||||||
|
color: 'text-yellow-500',
|
||||||
|
},
|
||||||
|
INBOX: {
|
||||||
|
label: 'Inbox',
|
||||||
|
icon: SignatureIcon,
|
||||||
|
color: 'text-muted-foreground',
|
||||||
|
},
|
||||||
|
ALL: {
|
||||||
|
label: 'All',
|
||||||
|
color: 'text-muted-foreground',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
||||||
status: InternalDocumentStatus;
|
status: ExtendedDocumentStatus;
|
||||||
inheritColor?: boolean;
|
inheritColor?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,11 +55,13 @@ export const DocumentStatus = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn('flex items-center', className)} {...props}>
|
<span className={cn('flex items-center', className)} {...props}>
|
||||||
<Icon
|
{Icon && (
|
||||||
className={cn('mr-2 inline-block h-4 w-4', {
|
<Icon
|
||||||
[color]: !inheritColor,
|
className={cn('mr-2 inline-block h-4 w-4', {
|
||||||
})}
|
[color]: !inheritColor,
|
||||||
/>
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
||||||
|
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
||||||
import { TAddFieldsFormSchema } from './add-fields.types';
|
|
||||||
|
|
||||||
export type AddFieldsActionInput = TAddFieldsFormSchema & {
|
export type AddFieldsActionInput = TAddFieldsFormSchema & {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||||
|
import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
|
||||||
import { TAddSignersFormSchema } from './add-signers.types';
|
|
||||||
|
|
||||||
export type AddSignersActionInput = TAddSignersFormSchema & {
|
export type AddSignersActionInput = TAddSignersFormSchema & {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
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 './add-subject.types';
|
|
||||||
|
|
||||||
export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import { FormErrorMessage } from '../form/form-error-message';
|
|||||||
|
|
||||||
export const ZPasswordFormSchema = z
|
export const ZPasswordFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
password: z.string().min(6),
|
password: z.string().min(6).max(72),
|
||||||
repeatedPassword: z.string().min(6),
|
repeatedPassword: z.string().min(6).max(72),
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.repeatedPassword, {
|
.refine((data) => data.password === data.repeatedPassword, {
|
||||||
message: 'Passwords do not match',
|
message: 'Passwords do not match',
|
||||||
@@ -92,6 +92,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
minLength={6}
|
||||||
|
maxLength={72}
|
||||||
|
autoComplete="new-password"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
@@ -107,6 +110,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
<Input
|
<Input
|
||||||
id="repeated-password"
|
id="repeated-password"
|
||||||
type="password"
|
type="password"
|
||||||
|
minLength={6}
|
||||||
|
maxLength={72}
|
||||||
|
autoComplete="new-password"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
{...register('repeatedPassword')}
|
{...register('repeatedPassword')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import { cn } from '@documenso/ui/lib/utils';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
import { SignaturePad } from '../signature-pad';
|
|
||||||
|
|
||||||
export const ZProfileFormSchema = z.object({
|
export const ZProfileFormSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
export const ZSignInFormSchema = z.object({
|
export const ZSignInFormSchema = z.object({
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
password: z.string().min(1),
|
password: z.string().min(6).max(72),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSignInFormSchema = z.infer<typeof ZSignInFormSchema>;
|
export type TSignInFormSchema = z.infer<typeof ZSignInFormSchema>;
|
||||||
@@ -44,7 +44,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
await signIn('credentials', {
|
await signIn('credentials', {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
callbackUrl: '/dashboard',
|
callbackUrl: '/documents',
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
@@ -99,6 +99,9 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
minLength={6}
|
||||||
|
maxLength={72}
|
||||||
|
autoComplete="current-password"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,14 +12,13 @@ import { cn } from '@documenso/ui/lib/utils';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { SignaturePad } from '../signature-pad';
|
|
||||||
|
|
||||||
export const ZSignUpFormSchema = z.object({
|
export const ZSignUpFormSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
password: z.string().min(1),
|
password: z.string().min(6).max(72),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
||||||
@@ -106,6 +105,9 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
minLength={6}
|
||||||
|
maxLength={72}
|
||||||
|
autoComplete="new-password"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,321 +0,0 @@
|
|||||||
import { Point } from './point';
|
|
||||||
|
|
||||||
export class Canvas {
|
|
||||||
private readonly $canvas: HTMLCanvasElement;
|
|
||||||
private readonly $offscreenCanvas: HTMLCanvasElement;
|
|
||||||
|
|
||||||
private currentCanvasWidth = 0;
|
|
||||||
private currentCanvasHeight = 0;
|
|
||||||
|
|
||||||
private points: Point[] = [];
|
|
||||||
private onChangeHandlers: Array<(_canvas: Canvas, _cleared: boolean) => void> = [];
|
|
||||||
|
|
||||||
private isPressed = false;
|
|
||||||
private lastVelocity = 0;
|
|
||||||
|
|
||||||
private readonly VELOCITY_FILTER_WEIGHT = 0.5;
|
|
||||||
private readonly DPI = 2;
|
|
||||||
|
|
||||||
constructor(canvas: HTMLCanvasElement) {
|
|
||||||
this.$canvas = canvas;
|
|
||||||
this.$offscreenCanvas = document.createElement('canvas');
|
|
||||||
|
|
||||||
const { width, height } = this.$canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
this.currentCanvasWidth = width * this.DPI;
|
|
||||||
this.currentCanvasHeight = height * this.DPI;
|
|
||||||
|
|
||||||
this.$canvas.width = this.currentCanvasWidth;
|
|
||||||
this.$canvas.height = this.currentCanvasHeight;
|
|
||||||
|
|
||||||
Object.assign(this.$canvas.style, {
|
|
||||||
touchAction: 'none',
|
|
||||||
msTouchAction: 'none',
|
|
||||||
userSelect: 'none',
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('resize', this.onResize.bind(this));
|
|
||||||
|
|
||||||
this.$canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
|
|
||||||
this.$canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
|
|
||||||
this.$canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
|
|
||||||
this.$canvas.addEventListener('mouseenter', this.onMouseEnter.bind(this));
|
|
||||||
this.$canvas.addEventListener('mouseleave', this.onMouseLeave.bind(this));
|
|
||||||
this.$canvas.addEventListener('pointerdown', this.onMouseDown.bind(this));
|
|
||||||
this.$canvas.addEventListener('pointermove', this.onMouseMove.bind(this));
|
|
||||||
this.$canvas.addEventListener('pointerup', this.onMouseUp.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the minimum stroke width as a percentage of the current canvas suitable for a signature.
|
|
||||||
*/
|
|
||||||
private minStrokeWidth(): number {
|
|
||||||
return Math.min(this.currentCanvasWidth, this.currentCanvasHeight) * 0.005;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the maximum stroke width as a percentage of the current canvas suitable for a signature.
|
|
||||||
*/
|
|
||||||
private maxStrokeWidth(): number {
|
|
||||||
return Math.min(this.currentCanvasWidth, this.currentCanvasHeight) * 0.035;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the HTML canvas element.
|
|
||||||
*/
|
|
||||||
public getCanvas(): HTMLCanvasElement {
|
|
||||||
return this.$canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the 2D rendering context of the canvas.
|
|
||||||
* Throws an error if the context is not available.
|
|
||||||
*/
|
|
||||||
public getContext(): CanvasRenderingContext2D {
|
|
||||||
const ctx = this.$canvas.getContext('2d');
|
|
||||||
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('Canvas context is not available.');
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.imageSmoothingQuality = 'high';
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the resize event of the canvas.
|
|
||||||
* Adjusts the canvas size and preserves the content using image data.
|
|
||||||
*/
|
|
||||||
private onResize(): void {
|
|
||||||
const { width, height } = this.$canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
const oldWidth = this.currentCanvasWidth;
|
|
||||||
const oldHeight = this.currentCanvasHeight;
|
|
||||||
|
|
||||||
const ctx = this.getContext();
|
|
||||||
|
|
||||||
const imageData = ctx.getImageData(0, 0, oldWidth, oldHeight);
|
|
||||||
|
|
||||||
this.$canvas.width = width * this.DPI;
|
|
||||||
this.$canvas.height = height * this.DPI;
|
|
||||||
|
|
||||||
this.currentCanvasWidth = width * this.DPI;
|
|
||||||
this.currentCanvasHeight = height * this.DPI;
|
|
||||||
|
|
||||||
ctx.putImageData(imageData, 0, 0, 0, 0, width * this.DPI, height * this.DPI);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the mouse down event on the canvas.
|
|
||||||
* Adds the starting point for the signature.
|
|
||||||
*/
|
|
||||||
private onMouseDown(event: MouseEvent | PointerEvent | TouchEvent): void {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isPressed = true;
|
|
||||||
|
|
||||||
const point = Point.fromEvent(event, this.DPI);
|
|
||||||
|
|
||||||
this.addPoint(point);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the mouse move event on the canvas.
|
|
||||||
* Adds a point to the signature if the mouse is pressed, based on the sample rate.
|
|
||||||
*/
|
|
||||||
private onMouseMove(event: MouseEvent | PointerEvent | TouchEvent): void {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isPressed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const point = Point.fromEvent(event, this.DPI);
|
|
||||||
|
|
||||||
if (point.distanceTo(this.points[this.points.length - 1]) > 10) {
|
|
||||||
this.addPoint(point);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the mouse up event on the canvas.
|
|
||||||
* Adds the final point for the signature and resets the points array.
|
|
||||||
*/
|
|
||||||
private onMouseUp(event: MouseEvent | PointerEvent | TouchEvent, addPoint = true): void {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isPressed = false;
|
|
||||||
|
|
||||||
const point = Point.fromEvent(event, this.DPI);
|
|
||||||
|
|
||||||
if (addPoint) {
|
|
||||||
this.addPoint(point);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onChangeHandlers.forEach((handler) => handler(this, false));
|
|
||||||
|
|
||||||
this.points = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMouseEnter(event: MouseEvent): void {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
event.buttons === 1 && this.onMouseDown(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMouseLeave(event: MouseEvent): void {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onMouseUp(event, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a point to the signature and performs smoothing and drawing.
|
|
||||||
*/
|
|
||||||
private addPoint(point: Point): void {
|
|
||||||
const lastPoint = this.points[this.points.length - 1] ?? point;
|
|
||||||
|
|
||||||
this.points.push(point);
|
|
||||||
|
|
||||||
const smoothedPoints = this.smoothSignature(this.points);
|
|
||||||
|
|
||||||
let velocity = point.velocityFrom(lastPoint);
|
|
||||||
velocity =
|
|
||||||
this.VELOCITY_FILTER_WEIGHT * velocity +
|
|
||||||
(1 - this.VELOCITY_FILTER_WEIGHT) * this.lastVelocity;
|
|
||||||
|
|
||||||
const newWidth =
|
|
||||||
velocity > 0 && this.lastVelocity > 0 ? this.strokeWidth(velocity) : this.minStrokeWidth();
|
|
||||||
|
|
||||||
this.drawSmoothSignature(smoothedPoints, newWidth);
|
|
||||||
|
|
||||||
this.lastVelocity = velocity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a smoothing algorithm to the signature points.
|
|
||||||
*/
|
|
||||||
private smoothSignature(points: Point[]): Point[] {
|
|
||||||
const smoothedPoints: Point[] = [];
|
|
||||||
|
|
||||||
const startPoint = points[0];
|
|
||||||
const endPoint = points[points.length - 1];
|
|
||||||
|
|
||||||
smoothedPoints.push(startPoint);
|
|
||||||
|
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
|
||||||
const p0 = i > 0 ? points[i - 1] : startPoint;
|
|
||||||
const p1 = points[i];
|
|
||||||
const p2 = points[i + 1];
|
|
||||||
const p3 = i < points.length - 2 ? points[i + 2] : endPoint;
|
|
||||||
|
|
||||||
const cp1x = p1.x + (p2.x - p0.x) / 6;
|
|
||||||
const cp1y = p1.y + (p2.y - p0.y) / 6;
|
|
||||||
|
|
||||||
const cp2x = p2.x - (p3.x - p1.x) / 6;
|
|
||||||
const cp2y = p2.y - (p3.y - p1.y) / 6;
|
|
||||||
|
|
||||||
smoothedPoints.push(new Point(cp1x, cp1y));
|
|
||||||
smoothedPoints.push(new Point(cp2x, cp2y));
|
|
||||||
smoothedPoints.push(p2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return smoothedPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws the smoothed signature on the canvas.
|
|
||||||
*/
|
|
||||||
private drawSmoothSignature(points: Point[], width: number): void {
|
|
||||||
const ctx = this.getContext();
|
|
||||||
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
|
|
||||||
const startPoint = points[0];
|
|
||||||
|
|
||||||
ctx.moveTo(startPoint.x, startPoint.y);
|
|
||||||
|
|
||||||
ctx.lineWidth = width;
|
|
||||||
|
|
||||||
for (let i = 1; i < points.length; i += 3) {
|
|
||||||
const cp1 = points[i];
|
|
||||||
const cp2 = points[i + 1];
|
|
||||||
const endPoint = points[i + 2];
|
|
||||||
|
|
||||||
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, endPoint.x, endPoint.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.closePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the stroke width based on the velocity.
|
|
||||||
*/
|
|
||||||
private strokeWidth(velocity: number): number {
|
|
||||||
return Math.max(this.maxStrokeWidth() / (velocity + 1), this.minStrokeWidth());
|
|
||||||
}
|
|
||||||
|
|
||||||
public registerOnChangeHandler(handler: (_canvas: Canvas, _cleared: boolean) => void): void {
|
|
||||||
this.onChangeHandlers.push(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unregisterOnChangeHandler(handler: (_canvas: Canvas, _cleared: boolean) => void): void {
|
|
||||||
this.onChangeHandlers = this.onChangeHandlers.filter((l) => l !== handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the signature as a data URL.
|
|
||||||
*/
|
|
||||||
public toDataURL(type?: string, quality?: number): string {
|
|
||||||
return this.$canvas.toDataURL(type, quality);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the signature from the canvas.
|
|
||||||
*/
|
|
||||||
public clear(): void {
|
|
||||||
const ctx = this.getContext();
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, this.currentCanvasWidth, this.currentCanvasHeight);
|
|
||||||
|
|
||||||
this.onChangeHandlers.forEach((handler) => handler(this, true));
|
|
||||||
|
|
||||||
this.points = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the signature as an image blob.
|
|
||||||
*/
|
|
||||||
public toBlob(type?: string, quality?: number): Promise<Blob> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.$canvas.toBlob(
|
|
||||||
(blob) => {
|
|
||||||
if (!blob) {
|
|
||||||
reject(new Error('Could not convert canvas to blob.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(blob);
|
|
||||||
},
|
|
||||||
type,
|
|
||||||
quality,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
export const average = (a: number, b: number) => (a + b) / 2;
|
|
||||||
|
|
||||||
export const getSvgPathFromStroke = (points: number[][], closed = true) => {
|
|
||||||
const len = points.length;
|
|
||||||
|
|
||||||
if (len < 4) {
|
|
||||||
return ``;
|
|
||||||
}
|
|
||||||
|
|
||||||
let a = points[0];
|
|
||||||
let b = points[1];
|
|
||||||
const c = points[2];
|
|
||||||
|
|
||||||
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(2)},${b[1].toFixed(
|
|
||||||
2,
|
|
||||||
)} ${average(b[0], c[0]).toFixed(2)},${average(b[1], c[1]).toFixed(2)} T`;
|
|
||||||
|
|
||||||
for (let i = 2, max = len - 1; i < max; i++) {
|
|
||||||
a = points[i];
|
|
||||||
b = points[i + 1];
|
|
||||||
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(2)} `;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closed) {
|
|
||||||
result += 'Z';
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './signature-pad';
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import {
|
|
||||||
MouseEvent as ReactMouseEvent,
|
|
||||||
PointerEvent as ReactPointerEvent,
|
|
||||||
TouchEvent as ReactTouchEvent,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
export type PointLike = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
timestamp: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isTouchEvent = (
|
|
||||||
event:
|
|
||||||
| ReactMouseEvent
|
|
||||||
| ReactPointerEvent
|
|
||||||
| ReactTouchEvent
|
|
||||||
| MouseEvent
|
|
||||||
| PointerEvent
|
|
||||||
| TouchEvent,
|
|
||||||
): event is TouchEvent | ReactTouchEvent => {
|
|
||||||
return 'touches' in event;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Point implements PointLike {
|
|
||||||
public x: number;
|
|
||||||
public y: number;
|
|
||||||
public timestamp: number;
|
|
||||||
|
|
||||||
constructor(x: number, y: number, timestamp?: number) {
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.timestamp = timestamp ?? Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public distanceTo(point: PointLike): number {
|
|
||||||
return Math.sqrt(Math.pow(point.x - this.x, 2) + Math.pow(point.y - this.y, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
public equals(point: PointLike): boolean {
|
|
||||||
return this.x === point.x && this.y === point.y && this.timestamp === point.timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public velocityFrom(start: PointLike): number {
|
|
||||||
const timeDifference = this.timestamp - start.timestamp;
|
|
||||||
|
|
||||||
if (timeDifference !== 0) {
|
|
||||||
return this.distanceTo(start) / timeDifference;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromPointLike({ x, y, timestamp }: PointLike): Point {
|
|
||||||
return new Point(x, y, timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromEvent(
|
|
||||||
event:
|
|
||||||
| ReactMouseEvent
|
|
||||||
| ReactPointerEvent
|
|
||||||
| ReactTouchEvent
|
|
||||||
| MouseEvent
|
|
||||||
| PointerEvent
|
|
||||||
| TouchEvent,
|
|
||||||
dpi = 1,
|
|
||||||
el?: HTMLElement | null,
|
|
||||||
): Point {
|
|
||||||
const target = el ?? event.target;
|
|
||||||
|
|
||||||
if (!(target instanceof HTMLElement)) {
|
|
||||||
throw new Error('Event target is not an HTMLElement.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { top, bottom, left, right } = target.getBoundingClientRect();
|
|
||||||
|
|
||||||
let clientX, clientY;
|
|
||||||
|
|
||||||
if (isTouchEvent(event)) {
|
|
||||||
clientX = event.touches[0].clientX;
|
|
||||||
clientY = event.touches[0].clientY;
|
|
||||||
} else {
|
|
||||||
clientX = event.clientX;
|
|
||||||
clientY = event.clientY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new point snapping to the edge of the current target element if it exceeds
|
|
||||||
// the bounding box of the target element
|
|
||||||
let x = Math.min(Math.max(left, clientX), right) - left;
|
|
||||||
let y = Math.min(Math.max(top, clientY), bottom) - top;
|
|
||||||
|
|
||||||
// adjust for DPI
|
|
||||||
x *= dpi;
|
|
||||||
y *= dpi;
|
|
||||||
|
|
||||||
return new Point(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
apps/web/src/hooks/use-debounced-value.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useDebouncedValue<T>(value: T, delay: number) {
|
||||||
|
// State and setters for debounced value
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||||
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { Field } from '@documenso/prisma/client';
|
import { Field } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
|
|
||||||
import { getBoundingClientRect } from '~/helpers/get-bounding-client-rect';
|
|
||||||
|
|
||||||
export const useFieldPageCoords = (field: Field) => {
|
export const useFieldPageCoords = (field: Field) => {
|
||||||
const [coords, setCoords] = useState({
|
const [coords, setCoords] = useState({
|
||||||
x: 0,
|
x: 0,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
export default async function middleware(req: NextRequest) {
|
export default async function middleware(req: NextRequest) {
|
||||||
if (req.nextUrl.pathname === '/') {
|
if (req.nextUrl.pathname === '/') {
|
||||||
const redirectUrl = new URL('/dashboard', req.url);
|
const redirectUrl = new URL('/documents', req.url);
|
||||||
|
|
||||||
return NextResponse.redirect(redirectUrl);
|
return NextResponse.redirect(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ module.exports = {
|
|||||||
content: [
|
content: [
|
||||||
...baseConfig.content,
|
...baseConfig.content,
|
||||||
`${path.join(require.resolve('@documenso/ui'), '..')}/**/*.{ts,tsx}`,
|
`${path.join(require.resolve('@documenso/ui'), '..')}/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/email'), '..')}/**/*.{ts,tsx}`,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,16 +9,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": [
|
"~/*": ["./src/*"],
|
||||||
"./src/*"
|
"contentlayer/generated": ["./.contentlayer/generated"]
|
||||||
],
|
|
||||||
"contentlayer/generated": [
|
|
||||||
"./.contentlayer/generated"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"types": [
|
"types": ["@documenso/lib/types/next-auth.d.ts"],
|
||||||
"@documenso/lib/types/next-auth.d.ts"
|
|
||||||
],
|
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"incremental": false
|
"incremental": false
|
||||||
},
|
},
|
||||||
@@ -30,7 +24,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".contentlayer/generated"
|
".contentlayer/generated"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules"]
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
448
package-lock.json
generated
@@ -23,6 +23,10 @@
|
|||||||
"lint-staged": "^14.0.0",
|
"lint-staged": "^14.0.0",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"turbo": "^1.9.3"
|
"turbo": "^1.9.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=8.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/marketing": {
|
"apps/marketing": {
|
||||||
@@ -49,6 +53,7 @@
|
|||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"recharts": "^2.7.2",
|
"recharts": "^2.7.2",
|
||||||
|
"sharp": "0.32.5",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
@@ -73,6 +78,7 @@
|
|||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.214.0",
|
"lucide-react": "^0.214.0",
|
||||||
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"next": "13.4.12",
|
"next": "13.4.12",
|
||||||
@@ -87,14 +93,15 @@
|
|||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-pdf": "^7.1.1",
|
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
|
"sharp": "0.32.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/formidable": "^2.0.6",
|
"@types/formidable": "^2.0.6",
|
||||||
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/node": "20.1.0",
|
"@types/node": "20.1.0",
|
||||||
"@types/react": "18.2.18",
|
"@types/react": "18.2.18",
|
||||||
"@types/react-dom": "18.2.7"
|
"@types/react-dom": "18.2.7"
|
||||||
@@ -5210,6 +5217,11 @@
|
|||||||
"dequal": "^2.0.3"
|
"dequal": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/b4a": {
|
||||||
|
"version": "1.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
|
||||||
|
"integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw=="
|
||||||
|
},
|
||||||
"node_modules/bail": {
|
"node_modules/bail": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||||
@@ -5224,6 +5236,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/bcrypt": {
|
"node_modules/bcrypt": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz",
|
||||||
@@ -5253,6 +5284,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.5.0",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bplist-parser": {
|
"node_modules/bplist-parser": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
|
||||||
@@ -5315,6 +5356,29 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
@@ -5991,6 +6055,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/color": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1",
|
||||||
|
"color-string": "^1.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -6007,6 +6083,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/color-string": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "^1.0.0",
|
||||||
|
"simple-swizzle": "^0.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-support": {
|
"node_modules/color-support": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||||
@@ -6541,6 +6626,14 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deep-extend": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@@ -6894,6 +6987,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.15.0",
|
"version": "5.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
|
||||||
@@ -7971,6 +8072,14 @@
|
|||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expand-template": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/extend": {
|
"node_modules/extend": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
@@ -8005,6 +8114,11 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-fifo": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||||
@@ -8255,6 +8369,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-constants": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
|
||||||
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "11.1.1",
|
"version": "11.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
|
||||||
@@ -8465,6 +8584,11 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/github-from-package": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.1.7",
|
"version": "7.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
|
||||||
@@ -8983,6 +9107,25 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.2.4",
|
"version": "5.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||||
@@ -11364,6 +11507,11 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mkdirp-classic": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
||||||
|
},
|
||||||
"node_modules/moo": {
|
"node_modules/moo": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
|
||||||
@@ -11415,6 +11563,11 @@
|
|||||||
"node": "^14 || ^16 || >=18"
|
"node": "^14 || ^16 || >=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/napi-build-utils": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
|
||||||
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
@@ -11615,6 +11768,17 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-abi": {
|
||||||
|
"version": "3.47.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz",
|
||||||
|
"integrity": "sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-addon-api": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||||
@@ -12443,6 +12607,111 @@
|
|||||||
"preact": ">=10"
|
"preact": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prebuild-install": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.0",
|
||||||
|
"expand-template": "^2.0.3",
|
||||||
|
"github-from-package": "0.0.0",
|
||||||
|
"minimist": "^1.2.3",
|
||||||
|
"mkdirp-classic": "^0.5.3",
|
||||||
|
"napi-build-utils": "^1.0.1",
|
||||||
|
"node-abi": "^3.3.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"rc": "^1.2.7",
|
||||||
|
"simple-get": "^4.0.0",
|
||||||
|
"tar-fs": "^2.0.0",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prebuild-install": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prebuild-install/node_modules/chownr": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||||
|
},
|
||||||
|
"node_modules/prebuild-install/node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prebuild-install/node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prebuild-install/node_modules/simple-get": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"once": "^1.3.1",
|
||||||
|
"simple-concat": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prebuild-install/node_modules/tar-fs": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prebuild-install/node_modules/tar-stream": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.0.3",
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"fs-constants": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -12657,6 +12926,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
||||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
|
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
|
||||||
},
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||||
@@ -12698,6 +12976,11 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/queue-tick": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
|
||||||
|
},
|
||||||
"node_modules/quick-lru": {
|
"node_modules/quick-lru": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||||
@@ -12740,6 +13023,28 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rc": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-extend": "^0.6.0",
|
||||||
|
"ini": "~1.3.0",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"strip-json-comments": "~2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rc": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rc/node_modules/strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/re-resizable": {
|
"node_modules/re-resizable": {
|
||||||
"version": "6.9.6",
|
"version": "6.9.6",
|
||||||
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz",
|
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz",
|
||||||
@@ -12851,9 +13156,9 @@
|
|||||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||||
},
|
},
|
||||||
"node_modules/react-pdf": {
|
"node_modules/react-pdf": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.3.tgz",
|
||||||
"integrity": "sha512-FRUqxGqh/l9TfND7T9ooW5ulZZt1tPYVwSEonFv3ImQFp3OkPM09BljchwEGHszP7P6h/Tt8js6v6d/T7GTC3A==",
|
"integrity": "sha512-d7WAxcsjOogJfJ+I+zX/mdip3VjR1yq/yDa4hax4XbQVjbbbup6rqs4c8MGx0MLSnzob17TKp1t4CsNbDZ6GeQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"make-cancellable-promise": "^1.3.1",
|
"make-cancellable-promise": "^1.3.1",
|
||||||
@@ -13792,6 +14097,82 @@
|
|||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/sharp": {
|
||||||
|
"version": "0.32.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.5.tgz",
|
||||||
|
"integrity": "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"color": "^4.2.3",
|
||||||
|
"detect-libc": "^2.0.2",
|
||||||
|
"node-addon-api": "^6.1.0",
|
||||||
|
"prebuild-install": "^7.1.1",
|
||||||
|
"semver": "^7.5.4",
|
||||||
|
"simple-get": "^4.0.1",
|
||||||
|
"tar-fs": "^3.0.4",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.15.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sharp/node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sharp/node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sharp/node_modules/node-addon-api": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="
|
||||||
|
},
|
||||||
|
"node_modules/sharp/node_modules/simple-get": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"once": "^1.3.1",
|
||||||
|
"simple-concat": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -13846,8 +14227,7 @@
|
|||||||
"type": "consulting",
|
"type": "consulting",
|
||||||
"url": "https://feross.org/support"
|
"url": "https://feross.org/support"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/simple-get": {
|
"node_modules/simple-get": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
@@ -13860,6 +14240,19 @@
|
|||||||
"simple-concat": "^1.0.0"
|
"simple-concat": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-swizzle": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arrayish": "^0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
||||||
|
},
|
||||||
"node_modules/slash": {
|
"node_modules/slash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
@@ -14006,6 +14399,15 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/streamx": {
|
||||||
|
"version": "2.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz",
|
||||||
|
"integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-fifo": "^1.1.0",
|
||||||
|
"queue-tick": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
@@ -14469,6 +14871,26 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==",
|
||||||
|
"dependencies": {
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^3.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "3.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz",
|
||||||
|
"integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4",
|
||||||
|
"fast-fifo": "^1.2.0",
|
||||||
|
"streamx": "^2.15.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-extensions": {
|
"node_modules/text-extensions": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz",
|
||||||
@@ -14910,6 +15332,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/turbo": {
|
"node_modules/turbo": {
|
||||||
"version": "1.10.12",
|
"version": "1.10.12",
|
||||||
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.12.tgz",
|
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.12.tgz",
|
||||||
@@ -16197,7 +16630,10 @@
|
|||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.214.0",
|
"lucide-react": "^0.214.0",
|
||||||
|
"next": "13.4.12",
|
||||||
|
"pdfjs-dist": "3.6.172",
|
||||||
"react-day-picker": "^8.7.1",
|
"react-day-picker": "^8.7.1",
|
||||||
|
"react-pdf": "^7.3.3",
|
||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
"tailwindcss-animate": "^1.0.5"
|
"tailwindcss-animate": "^1.0.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"commitlint": "commitlint --edit"
|
"commitlint": "commitlint --edit"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"npm": ">=8.6.0",
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.7.1",
|
"@commitlint/cli": "^17.7.1",
|
||||||
"@commitlint/config-conventional": "^17.7.0",
|
"@commitlint/config-conventional": "^17.7.0",
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
export interface TemplateDocumentCompletedProps {
|
||||||
|
downloadLink: string;
|
||||||
|
reviewLink: string;
|
||||||
|
documentName: string;
|
||||||
|
assetBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateDocumentCompleted = ({
|
||||||
|
downloadLink,
|
||||||
|
reviewLink,
|
||||||
|
documentName,
|
||||||
|
assetBaseUrl,
|
||||||
|
}: TemplateDocumentCompletedProps) => {
|
||||||
|
const getAssetUrl = (path: string) => {
|
||||||
|
return new URL(path, assetBaseUrl).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: config.theme.extend.colors,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Section className="flex-row items-center justify-center">
|
||||||
|
<div className="flex items-center justify-center p-4">
|
||||||
|
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
|
||||||
|
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||||
|
Completed
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||||
|
“{documentName}” was signed by all signers
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
|
Continue by downloading or reviewing the document.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section className="mb-6 mt-8 text-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||||
|
href={reviewLink}
|
||||||
|
>
|
||||||
|
<Img src={getAssetUrl('/static/review.png')} className="-mb-1 mr-2 inline h-5 w-5" />
|
||||||
|
Review
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||||
|
href={downloadLink}
|
||||||
|
>
|
||||||
|
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
</Tailwind>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateDocumentCompleted;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
export interface TemplateDocumentInviteProps {
|
||||||
|
inviterName: string;
|
||||||
|
inviterEmail: string;
|
||||||
|
documentName: string;
|
||||||
|
signDocumentLink: string;
|
||||||
|
assetBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateDocumentInvite = ({
|
||||||
|
inviterName,
|
||||||
|
documentName,
|
||||||
|
signDocumentLink,
|
||||||
|
assetBaseUrl,
|
||||||
|
}: TemplateDocumentInviteProps) => {
|
||||||
|
const getAssetUrl = (path: string) => {
|
||||||
|
return new URL(path, assetBaseUrl).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: config.theme.extend.colors,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Section className="mt-4 flex-row items-center justify-center">
|
||||||
|
<div className="flex items-center justify-center p-4">
|
||||||
|
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
|
{inviterName} has invited you to sign "{documentName}"
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
|
Continue by signing the document.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section className="mb-6 mt-8 text-center">
|
||||||
|
<Button
|
||||||
|
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
|
||||||
|
href={signDocumentLink}
|
||||||
|
>
|
||||||
|
Sign Document
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
</Tailwind>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateDocumentInvite;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Img, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
export interface TemplateDocumentPendingProps {
|
||||||
|
documentName: string;
|
||||||
|
assetBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateDocumentPending = ({
|
||||||
|
documentName,
|
||||||
|
assetBaseUrl,
|
||||||
|
}: TemplateDocumentPendingProps) => {
|
||||||
|
const getAssetUrl = (path: string) => {
|
||||||
|
return new URL(path, assetBaseUrl).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: config.theme.extend.colors,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Section className="flex-row items-center justify-center">
|
||||||
|
<div className="flex items-center justify-center p-4">
|
||||||
|
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500">
|
||||||
|
<Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||||
|
Waiting for others
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||||
|
“{documentName}” has been signed
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
|
||||||
|
We're still waiting for other signers to sign this document.
|
||||||
|
<br />
|
||||||
|
We'll notify you as soon as it's ready.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Tailwind>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateDocumentPending;
|
||||||
22
packages/email/template-components/template-footer.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Link, Section, Text } from '@react-email/components';
|
||||||
|
|
||||||
|
export const TemplateFooter = () => {
|
||||||
|
return (
|
||||||
|
<Section>
|
||||||
|
<Text className="my-4 text-base text-slate-400">
|
||||||
|
This document was sent using{' '}
|
||||||
|
<Link className="text-[#7AC455]" href="https://documenso.com">
|
||||||
|
Documenso.
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="my-8 text-sm text-slate-400">
|
||||||
|
Documenso
|
||||||
|
<br />
|
||||||
|
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateFooter;
|
||||||
@@ -1,25 +1,23 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Button,
|
|
||||||
Container,
|
Container,
|
||||||
Head,
|
Head,
|
||||||
Html,
|
Html,
|
||||||
Img,
|
Img,
|
||||||
Link,
|
|
||||||
Preview,
|
Preview,
|
||||||
Section,
|
Section,
|
||||||
Tailwind,
|
Tailwind,
|
||||||
Text,
|
|
||||||
} from '@react-email/components';
|
} from '@react-email/components';
|
||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
interface DocumentCompletedEmailTemplateProps {
|
import {
|
||||||
downloadLink?: string;
|
TemplateDocumentCompleted,
|
||||||
reviewLink?: string;
|
TemplateDocumentCompletedProps,
|
||||||
documentName?: string;
|
} from '../template-components/template-document-completed';
|
||||||
assetBaseUrl?: string;
|
import TemplateFooter from '../template-components/template-footer';
|
||||||
}
|
|
||||||
|
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;
|
||||||
|
|
||||||
export const DocumentCompletedEmailTemplate = ({
|
export const DocumentCompletedEmailTemplate = ({
|
||||||
downloadLink = 'https://documenso.com',
|
downloadLink = 'https://documenso.com',
|
||||||
@@ -50,74 +48,23 @@ export const DocumentCompletedEmailTemplate = ({
|
|||||||
<Section className="bg-white">
|
<Section className="bg-white">
|
||||||
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
|
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
|
||||||
<Section className="p-2">
|
<Section className="p-2">
|
||||||
<Img src={getAssetUrl('/static/logo.png')} alt="Documenso Logo" className="h-6" />
|
<Img
|
||||||
|
src={getAssetUrl('/static/logo.png')}
|
||||||
|
alt="Documenso Logo"
|
||||||
|
className="mb-4 h-6"
|
||||||
|
/>
|
||||||
|
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<TemplateDocumentCompleted
|
||||||
<div className="flex items-center justify-center p-4">
|
downloadLink={downloadLink}
|
||||||
<Img
|
reviewLink={reviewLink}
|
||||||
className="h-42"
|
documentName={documentName}
|
||||||
src={getAssetUrl('/static/document.png')}
|
assetBaseUrl={assetBaseUrl}
|
||||||
alt="Documenso"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
|
|
||||||
<Img
|
|
||||||
src={getAssetUrl('/static/completed.png')}
|
|
||||||
className="-mb-0.5 mr-2 inline h-7 w-7"
|
|
||||||
/>
|
|
||||||
Completed
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
|
||||||
“{documentName}” was signed by all signers
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
|
||||||
Continue by downloading or reviewing the document.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Section className="mb-6 mt-8 text-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
|
||||||
href={reviewLink}
|
|
||||||
>
|
|
||||||
<Img
|
|
||||||
src={getAssetUrl('/static/review.png')}
|
|
||||||
className="-mb-1 mr-2 inline h-5 w-5"
|
|
||||||
/>
|
|
||||||
Review
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
|
||||||
href={downloadLink}
|
|
||||||
>
|
|
||||||
<Img
|
|
||||||
src={getAssetUrl('/static/download.png')}
|
|
||||||
className="-mb-1 mr-2 inline h-5 w-5"
|
|
||||||
/>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Container className="mx-auto max-w-xl">
|
<Container className="mx-auto max-w-xl">
|
||||||
<Section>
|
<TemplateFooter />
|
||||||
<Text className="my-4 text-base text-slate-400">
|
|
||||||
This document was sent using{' '}
|
|
||||||
<Link className="text-[#7AC455]" href="https://documenso.com">
|
|
||||||
Documenso.
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="my-8 text-sm text-slate-400">
|
|
||||||
Documenso
|
|
||||||
<br />
|
|
||||||
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Section>
|
</Section>
|
||||||
</Body>
|
</Body>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Button,
|
|
||||||
Container,
|
Container,
|
||||||
Head,
|
Head,
|
||||||
Hr,
|
Hr,
|
||||||
@@ -15,13 +14,13 @@ import {
|
|||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
interface DocumentInviteEmailTemplateProps {
|
import {
|
||||||
inviterName?: string;
|
TemplateDocumentInvite,
|
||||||
inviterEmail?: string;
|
TemplateDocumentInviteProps,
|
||||||
documentName?: string;
|
} from '../template-components/template-document-invite';
|
||||||
signDocumentLink?: string;
|
import TemplateFooter from '../template-components/template-footer';
|
||||||
assetBaseUrl?: string;
|
|
||||||
}
|
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps>;
|
||||||
|
|
||||||
export const DocumentInviteEmailTemplate = ({
|
export const DocumentInviteEmailTemplate = ({
|
||||||
inviterName = 'Lucas Smith',
|
inviterName = 'Lucas Smith',
|
||||||
@@ -51,36 +50,21 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
>
|
>
|
||||||
<Body className="mx-auto my-auto bg-white font-sans">
|
<Body className="mx-auto my-auto bg-white font-sans">
|
||||||
<Section>
|
<Section>
|
||||||
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
|
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
|
||||||
<Section className="p-2">
|
<Section>
|
||||||
<Img src={getAssetUrl('/static/logo.png')} alt="Documenso Logo" className="h-6" />
|
<Img
|
||||||
|
src={getAssetUrl('/static/logo.png')}
|
||||||
|
alt="Documenso Logo"
|
||||||
|
className="mb-4 h-6"
|
||||||
|
/>
|
||||||
|
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<TemplateDocumentInvite
|
||||||
<div className="flex items-center justify-center p-4">
|
inviterName={inviterName}
|
||||||
<Img
|
inviterEmail={inviterEmail}
|
||||||
className="h-42"
|
documentName={documentName}
|
||||||
src={getAssetUrl('/static/document.png')}
|
signDocumentLink={signDocumentLink}
|
||||||
alt="Documenso"
|
assetBaseUrl={assetBaseUrl}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
|
||||||
{inviterName} has invited you to sign "{documentName}"
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
|
||||||
Continue by signing the document.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Section className="mb-6 mt-8 text-center">
|
|
||||||
<Button
|
|
||||||
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
|
|
||||||
href={signDocumentLink}
|
|
||||||
>
|
|
||||||
Sign Document
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
@@ -102,20 +86,7 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
<Hr className="mx-auto mt-12 max-w-xl" />
|
<Hr className="mx-auto mt-12 max-w-xl" />
|
||||||
|
|
||||||
<Container className="mx-auto max-w-xl">
|
<Container className="mx-auto max-w-xl">
|
||||||
<Section>
|
<TemplateFooter />
|
||||||
<Text className="my-4 text-base text-slate-400">
|
|
||||||
This document was sent using{' '}
|
|
||||||
<Link className="text-[#7AC455]" href="https://documenso.com">
|
|
||||||
Documenso.
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="my-8 text-sm text-slate-400">
|
|
||||||
Documenso
|
|
||||||
<br />
|
|
||||||
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Section>
|
</Section>
|
||||||
</Body>
|
</Body>
|
||||||
|
|||||||
@@ -4,19 +4,20 @@ import {
|
|||||||
Head,
|
Head,
|
||||||
Html,
|
Html,
|
||||||
Img,
|
Img,
|
||||||
Link,
|
|
||||||
Preview,
|
Preview,
|
||||||
Section,
|
Section,
|
||||||
Tailwind,
|
Tailwind,
|
||||||
Text,
|
|
||||||
} from '@react-email/components';
|
} from '@react-email/components';
|
||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
interface DocumentPendingEmailTemplateProps {
|
import {
|
||||||
documentName?: string;
|
TemplateDocumentPending,
|
||||||
assetBaseUrl?: string;
|
TemplateDocumentPendingProps,
|
||||||
}
|
} from '../template-components/template-document-pending';
|
||||||
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
|
|
||||||
|
export type DocumentPendingEmailTemplateProps = Partial<TemplateDocumentPendingProps>;
|
||||||
|
|
||||||
export const DocumentPendingEmailTemplate = ({
|
export const DocumentPendingEmailTemplate = ({
|
||||||
documentName = 'Open Source Pledge.pdf',
|
documentName = 'Open Source Pledge.pdf',
|
||||||
@@ -43,55 +44,20 @@ export const DocumentPendingEmailTemplate = ({
|
|||||||
>
|
>
|
||||||
<Body className="mx-auto my-auto font-sans">
|
<Body className="mx-auto my-auto font-sans">
|
||||||
<Section className="bg-white">
|
<Section className="bg-white">
|
||||||
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
|
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
|
||||||
<Section className="p-2">
|
<Section>
|
||||||
<Img src={getAssetUrl('/static/logo.png')} alt="Documenso Logo" className="h-6" />
|
<Img
|
||||||
|
src={getAssetUrl('/static/logo.png')}
|
||||||
|
alt="Documenso Logo"
|
||||||
|
className="mb-4 h-6"
|
||||||
|
/>
|
||||||
|
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<TemplateDocumentPending documentName={documentName} assetBaseUrl={assetBaseUrl} />
|
||||||
<div className="flex items-center justify-center p-4">
|
|
||||||
<Img
|
|
||||||
className="h-42"
|
|
||||||
src={getAssetUrl('/static/document.png')}
|
|
||||||
alt="Documenso"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500">
|
|
||||||
<Img
|
|
||||||
src={getAssetUrl('/static/clock.png')}
|
|
||||||
className="-mb-0.5 mr-2 inline h-7 w-7"
|
|
||||||
/>
|
|
||||||
Waiting for others
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
|
||||||
“{documentName}” has been signed
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
|
|
||||||
We're still waiting for other signers to sign this document.
|
|
||||||
<br />
|
|
||||||
We'll notify you as soon as it's ready.
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Container className="mx-auto max-w-xl">
|
<Container className="mx-auto max-w-xl">
|
||||||
<Section>
|
<TemplateFooter />
|
||||||
<Text className="my-4 text-base text-slate-400">
|
|
||||||
This document was sent using{' '}
|
|
||||||
<Link className="text-[#7AC455]" href="https://documenso.com">
|
|
||||||
Documenso.
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="my-8 text-sm text-slate-400">
|
|
||||||
Documenso
|
|
||||||
<br />
|
|
||||||
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Section>
|
</Section>
|
||||||
</Body>
|
</Body>
|
||||||
|
|||||||
93
packages/lib/client-only/hooks/use-document-element.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||||
|
|
||||||
|
export const useDocumentElement = () => {
|
||||||
|
/**
|
||||||
|
* Given a mouse event, find the nearest element found by the provided selector.
|
||||||
|
*/
|
||||||
|
const getPage = (event: MouseEvent, pageSelector: string) => {
|
||||||
|
if (!(event.target instanceof HTMLElement)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.target;
|
||||||
|
|
||||||
|
const $page =
|
||||||
|
target.closest<HTMLElement>(pageSelector) ?? target.querySelector<HTMLElement>(pageSelector);
|
||||||
|
|
||||||
|
if (!$page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $page;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provided a page and a field, calculate the position of the field
|
||||||
|
* as a percentage of the page width and height.
|
||||||
|
*/
|
||||||
|
const getFieldPosition = (page: HTMLElement, field: HTMLElement) => {
|
||||||
|
const {
|
||||||
|
top: pageTop,
|
||||||
|
left: pageLeft,
|
||||||
|
height: pageHeight,
|
||||||
|
width: pageWidth,
|
||||||
|
} = getBoundingClientRect(page);
|
||||||
|
|
||||||
|
const {
|
||||||
|
top: fieldTop,
|
||||||
|
left: fieldLeft,
|
||||||
|
height: fieldHeight,
|
||||||
|
width: fieldWidth,
|
||||||
|
} = getBoundingClientRect(field);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: ((fieldLeft - pageLeft) / pageWidth) * 100,
|
||||||
|
y: ((fieldTop - pageTop) / pageHeight) * 100,
|
||||||
|
width: (fieldWidth / pageWidth) * 100,
|
||||||
|
height: (fieldHeight / pageHeight) * 100,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a mouse event, determine if the mouse is within the bounds of the
|
||||||
|
* nearest element found by the provided selector.
|
||||||
|
*
|
||||||
|
* @param mouseWidth The artifical width of the mouse.
|
||||||
|
* @param mouseHeight The artifical height of the mouse.
|
||||||
|
*/
|
||||||
|
const isWithinPageBounds = useCallback(
|
||||||
|
(event: MouseEvent, pageSelector: string, mouseWidth = 0, mouseHeight = 0) => {
|
||||||
|
const $page = getPage(event, pageSelector);
|
||||||
|
|
||||||
|
if (!$page) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { top, left, height, width } = $page.getBoundingClientRect();
|
||||||
|
|
||||||
|
const halfMouseWidth = mouseWidth / 2;
|
||||||
|
const halfMouseHeight = mouseHeight / 2;
|
||||||
|
|
||||||
|
if (event.clientY > top + height - halfMouseHeight || event.clientY < top + halfMouseHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.clientX > left + width - halfMouseWidth || event.clientX < left + halfMouseWidth) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getPage,
|
||||||
|
getFieldPosition,
|
||||||
|
isWithinPageBounds,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import GoogleProvider, { GoogleProfile } from 'next-auth/providers/google';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
||||||
|
import { ErrorCodes } from './error-codes';
|
||||||
|
|
||||||
export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
@@ -23,21 +24,23 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
|||||||
},
|
},
|
||||||
authorize: async (credentials, _req) => {
|
authorize: async (credentials, _req) => {
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
return null;
|
throw new Error(ErrorCodes.CredentialsNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { email, password } = credentials;
|
const { email, password } = credentials;
|
||||||
|
|
||||||
const user = await getUserByEmail({ email }).catch(() => null);
|
const user = await getUserByEmail({ email }).catch(() => {
|
||||||
|
throw new Error(ErrorCodes.IncorrectEmailPassword);
|
||||||
|
});
|
||||||
|
|
||||||
if (!user || !user.password) {
|
if (!user.password) {
|
||||||
return null;
|
throw new Error(ErrorCodes.UserMissingPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPasswordsSame = compare(password, user.password);
|
const isPasswordsSame = await compare(password, user.password);
|
||||||
|
|
||||||
if (!isPasswordsSame) {
|
if (!isPasswordsSame) {
|
||||||
return null;
|
throw new Error(ErrorCodes.IncorrectEmailPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
5
packages/lib/next-auth/error-codes.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const ErrorCodes = {
|
||||||
|
IncorrectEmailPassword: 'incorrect-email-password',
|
||||||
|
UserMissingPassword: 'missing-password',
|
||||||
|
CredentialsNotFound: 'credentials-not-found',
|
||||||
|
} as const;
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { Document, DocumentStatus, Prisma } from '@documenso/prisma/client';
|
import { Document, Prisma, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
|
||||||
import { FindResultSet } from '../../types/find-result-set';
|
import { FindResultSet } from '../../types/find-result-set';
|
||||||
|
|
||||||
export interface FindDocumentsOptions {
|
export interface FindDocumentsOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
term?: string;
|
term?: string;
|
||||||
status?: DocumentStatus;
|
status?: ExtendedDocumentStatus;
|
||||||
page?: number;
|
page?: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
orderBy?: {
|
orderBy?: {
|
||||||
@@ -19,29 +21,102 @@ export interface FindDocumentsOptions {
|
|||||||
export const findDocuments = async ({
|
export const findDocuments = async ({
|
||||||
userId,
|
userId,
|
||||||
term,
|
term,
|
||||||
status,
|
status = ExtendedDocumentStatus.ALL,
|
||||||
page = 1,
|
page = 1,
|
||||||
perPage = 10,
|
perPage = 10,
|
||||||
orderBy,
|
orderBy,
|
||||||
}: FindDocumentsOptions): Promise<FindResultSet<DocumentWithReciepient>> => {
|
}: FindDocumentsOptions) => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const orderByColumn = orderBy?.column ?? 'created';
|
const orderByColumn = orderBy?.column ?? 'created';
|
||||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||||
|
|
||||||
const filters: Prisma.DocumentWhereInput = {
|
const termFilters = !term
|
||||||
status,
|
? undefined
|
||||||
userId,
|
: ({
|
||||||
};
|
title: {
|
||||||
|
contains: term,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
} as const);
|
||||||
|
|
||||||
if (term) {
|
const filters = match<ExtendedDocumentStatus, Prisma.DocumentWhereInput>(status)
|
||||||
filters.title = {
|
.with(ExtendedDocumentStatus.ALL, () => ({
|
||||||
contains: term,
|
OR: [
|
||||||
mode: 'insensitive',
|
{
|
||||||
};
|
userId,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
status: {
|
||||||
|
not: ExtendedDocumentStatus.DRAFT,
|
||||||
|
},
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
.with(ExtendedDocumentStatus.INBOX, () => ({
|
||||||
|
status: {
|
||||||
|
not: ExtendedDocumentStatus.DRAFT,
|
||||||
|
},
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
signingStatus: SigningStatus.NOT_SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.with(ExtendedDocumentStatus.DRAFT, () => ({
|
||||||
|
userId,
|
||||||
|
status: ExtendedDocumentStatus.DRAFT,
|
||||||
|
}))
|
||||||
|
.with(ExtendedDocumentStatus.PENDING, () => ({
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
status: ExtendedDocumentStatus.PENDING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: ExtendedDocumentStatus.PENDING,
|
||||||
|
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
.with(ExtendedDocumentStatus.COMPLETED, () => ({
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
status: ExtendedDocumentStatus.COMPLETED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: ExtendedDocumentStatus.COMPLETED,
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
.exhaustive();
|
||||||
|
|
||||||
const [data, count] = await Promise.all([
|
const [data, count] = await Promise.all([
|
||||||
prisma.document.findMany({
|
prisma.document.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
...termFilters,
|
||||||
...filters,
|
...filters,
|
||||||
},
|
},
|
||||||
skip: Math.max(page - 1, 0) * perPage,
|
skip: Math.max(page - 1, 0) * perPage,
|
||||||
@@ -50,21 +125,37 @@ export const findDocuments = async ({
|
|||||||
[orderByColumn]: orderByDirection,
|
[orderByColumn]: orderByDirection,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
User: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.document.count({
|
prisma.document.count({
|
||||||
where: {
|
where: {
|
||||||
|
...termFilters,
|
||||||
...filters,
|
...filters,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const maskedData = data.map((doc) => ({
|
||||||
|
...doc,
|
||||||
|
Recipient: doc.Recipient.map((recipient) => ({
|
||||||
|
...recipient,
|
||||||
|
token: recipient.email === user.email ? recipient.token : '',
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data: maskedData,
|
||||||
count,
|
count,
|
||||||
currentPage: Math.max(page, 1),
|
currentPage: Math.max(page, 1),
|
||||||
perPage,
|
perPage,
|
||||||
totalPages: Math.ceil(count / perPage),
|
totalPages: Math.ceil(count / perPage),
|
||||||
};
|
} satisfies FindResultSet<typeof maskedData>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,30 +1,88 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { SigningStatus, User } from '@documenso/prisma/client';
|
||||||
|
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||||
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
|
||||||
export type GetStatsInput = {
|
export type GetStatsInput = {
|
||||||
userId: number;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStats = async ({ userId }: GetStatsInput) => {
|
export const getStats = async ({ user }: GetStatsInput) => {
|
||||||
const result = await prisma.document.groupBy({
|
const [ownerCounts, notSignedCounts, hasSignedCounts] = await Promise.all([
|
||||||
by: ['status'],
|
prisma.document.groupBy({
|
||||||
_count: {
|
by: ['status'],
|
||||||
_all: true,
|
_count: {
|
||||||
},
|
_all: true,
|
||||||
where: {
|
},
|
||||||
userId,
|
where: {
|
||||||
},
|
userId: user.id,
|
||||||
});
|
},
|
||||||
|
}),
|
||||||
|
prisma.document.groupBy({
|
||||||
|
by: ['status'],
|
||||||
|
_count: {
|
||||||
|
_all: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
status: ExtendedDocumentStatus.PENDING,
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
signingStatus: SigningStatus.NOT_SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.document.groupBy({
|
||||||
|
by: ['status'],
|
||||||
|
_count: {
|
||||||
|
_all: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
status: {
|
||||||
|
not: ExtendedDocumentStatus.DRAFT,
|
||||||
|
},
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const stats: Record<DocumentStatus, number> = {
|
const stats: Record<ExtendedDocumentStatus, number> = {
|
||||||
[DocumentStatus.DRAFT]: 0,
|
[ExtendedDocumentStatus.DRAFT]: 0,
|
||||||
[DocumentStatus.PENDING]: 0,
|
[ExtendedDocumentStatus.PENDING]: 0,
|
||||||
[DocumentStatus.COMPLETED]: 0,
|
[ExtendedDocumentStatus.COMPLETED]: 0,
|
||||||
|
[ExtendedDocumentStatus.INBOX]: 0,
|
||||||
|
[ExtendedDocumentStatus.ALL]: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
result.forEach((stat) => {
|
ownerCounts.forEach((stat) => {
|
||||||
stats[stat.status] = stat._count._all;
|
stats[stat.status] = stat._count._all;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
notSignedCounts.forEach((stat) => {
|
||||||
|
stats[ExtendedDocumentStatus.INBOX] += stat._count._all;
|
||||||
|
});
|
||||||
|
|
||||||
|
hasSignedCounts.forEach((stat) => {
|
||||||
|
if (stat.status === ExtendedDocumentStatus.COMPLETED) {
|
||||||
|
stats[ExtendedDocumentStatus.COMPLETED] += stat._count._all;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.status === ExtendedDocumentStatus.PENDING) {
|
||||||
|
stats[ExtendedDocumentStatus.PENDING] += stat._count._all;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(stats).forEach((key) => {
|
||||||
|
if (key !== ExtendedDocumentStatus.ALL && isExtendedDocumentStatus(key)) {
|
||||||
|
stats[ExtendedDocumentStatus.ALL] += stats[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
signDocumentLink,
|
signDocumentLink,
|
||||||
});
|
});
|
||||||
|
|
||||||
mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
address: email,
|
address: email,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type FindResultSet<T> = {
|
export type FindResultSet<T> = {
|
||||||
data: T[];
|
data: T extends Array<any> ? T : T[];
|
||||||
count: number;
|
count: number;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
perPage: number;
|
perPage: number;
|
||||||
|
|||||||
11
packages/prisma/guards/is-extended-document-status.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { ExtendedDocumentStatus } from '../types/extended-document-status';
|
||||||
|
|
||||||
|
export const isExtendedDocumentStatus = (value: unknown): value is ExtendedDocumentStatus => {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're using the assertion for a type-guard so it's safe to ignore the eslint warning
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
return Object.values(ExtendedDocumentStatus).includes(value as ExtendedDocumentStatus);
|
||||||
|
};
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
previewFeatures = ["extendedWhereUnique", "jsonProtocol"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("NEXT_PRIVATE_DATABASE_URL")
|
url = env("NEXT_PRIVATE_DATABASE_URL")
|
||||||
|
directUrl = env("NEXT_PRIVATE_DIRECT_DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum IdentityProvider {
|
enum IdentityProvider {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Document, Recipient } from '@documenso/prisma/client';
|
import { Document, Recipient } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type DocumentWithReciepient = Document & {
|
export type DocumentWithRecipient = Document & {
|
||||||
Recipient: Recipient[];
|
Recipient: Recipient[];
|
||||||
};
|
};
|
||||||
|
|||||||
12
packages/prisma/types/document.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Document, Recipient } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export type DocumentWithRecipientAndSender = Omit<Document, 'document'> & {
|
||||||
|
recipient: Recipient;
|
||||||
|
sender: {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
subject: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
10
packages/prisma/types/extended-document-status.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { DocumentStatus } from '@prisma/client';
|
||||||
|
|
||||||
|
export const ExtendedDocumentStatus = {
|
||||||
|
...DocumentStatus,
|
||||||
|
INBOX: 'INBOX',
|
||||||
|
ALL: 'ALL',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ExtendedDocumentStatus =
|
||||||
|
(typeof ExtendedDocumentStatus)[keyof typeof ExtendedDocumentStatus];
|
||||||
@@ -115,6 +115,11 @@ module.exports = {
|
|||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
},
|
},
|
||||||
|
screens: {
|
||||||
|
'3xl': '1920px',
|
||||||
|
'4xl': '2560px',
|
||||||
|
'5xl': '3840px',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
|
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
|
||||||
|
|||||||