Compare commits

...

65 Commits

Author SHA1 Message Date
Mythie ca40e983e3 feat: promise safety with eslint 2023-08-29 13:01:19 +10:00
Lucas Smith ba054ae915 Merge pull request #319 from documenso/chore/reduce-refetch-time
chore: reduce open page caching time to 1 hour
2023-08-28 22:58:05 +10:00
Ephraim Atta-Duncan 1d1c6e5a55 chore: reduce open page caching time to 1 hour 2023-08-28 09:43:39 +00:00
Lucas Smith bf71d2a14e Merge pull request #305 from G3root/responsive-signing-card
fix: make signing form card responsive
2023-08-28 13:07:46 +10:00
Lucas Smith 163911255e Merge pull request #308 from adithyaakrishna/feat/pr-validate
feat: pr title validator workflow
2023-08-28 12:21:36 +10:00
Lucas Smith 24e38a3bbc Merge pull request #309 from nsylke/nsylke-patch-7
feat: set min/max lengths and autocomplete for password
2023-08-28 12:21:08 +10:00
Lucas Smith dfd714f16a Merge pull request #310 from adithyaakrishna/feat/add-sharp
feat: added sharp package for NextJS13 image optimizations
2023-08-28 12:19:33 +10:00
Mythie 722081f89e fix: dependency ordering 2023-08-28 12:14:15 +10:00
Lucas Smith f0e1df22b8 Merge pull request #312 from G3root/fix-auth
fix: authentication allowing any password
2023-08-28 12:04:07 +10:00
nafees nazik 615cb263fb fix: authentication 2023-08-27 07:10:41 +05:30
Adithya Krishna 650b69ae56 feat: added sharp for image optimizations on nextjs
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-26 21:23:30 +05:30
Adithya Krishna eb4be963e3 fix: added eol to workflow file
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-26 20:57:02 +05:30
Adithya Krishna 27c27743e3 chore: updated workflow name
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-26 20:57:02 +05:30
Adithya Krishna 92930a2f63 feat: added pr title validator
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-26 20:57:02 +05:30
nsylke 7ad3365b0e feat: add autocomplete for password managers 2023-08-26 10:22:44 -05:00
nsylke f8bf4fea36 feat: set min/max lengths for password 2023-08-26 09:53:58 -05:00
Lucas Smith 10cd8144eb Merge pull request #307 from adithyaakrishna/feat/optimize-images
chore: optimize images
2023-08-26 20:06:48 +10:00
Adithya Krishna 66973a3745 chore: optimized images to save ~8mb
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-26 13:13:30 +05:30
nafees nazik 85677bb792 fix: make signing form card responsive 2023-08-26 08:47:19 +05:30
Mythie 70a5105783 fix: add missing await on mail send 2023-08-25 18:33:24 +10:00
Mythie 420372ac9e fix: nicer dark mode for stack avatars 2023-08-25 17:52:58 +10:00
Mythie 6b00282a87 chore: support direct urls for prisma 2023-08-25 17:52:24 +10:00
Lucas Smith dae1001cbb Merge pull request #300 from documenso/feat/update-document-flow
feat: update document flow
2023-08-25 12:11:43 +10:00
David Nguyen af81d99b2a refactor: remove whitespace 2023-08-25 11:43:41 +10:00
David Nguyen 2751adc463 feat: update document flow
- Fixed z-index when dragging pre-existing fields
- Refactored document flow
- Added button spinner
- Added animation for document flow slider
- Updated drag and drop fields
- Updated document flow so it adjusts to the height of the PDF
- Updated claim plan dialog
2023-08-25 11:43:41 +10:00
Lucas Smith 396ce9f3f3 Merge pull request #295 from nsylke/nsylke-patch-6
fix: use -p cli option for next dev
2023-08-25 11:29:44 +10:00
Lucas Smith 3f4f66d878 Merge pull request #298 from adithyaakrishna/fix/dependabot
fix: dependabot workflow
2023-08-25 11:28:12 +10:00
Adithya Krishna d6751d7a26 fix: dependabot workflow
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-08-24 05:37:17 +00:00
Mythie 0e32baff0b feat: change document view upon completion 2023-08-24 13:31:50 +10:00
nsylke f76bf4c2c7 fix: use -p cli option for next dev 2023-08-23 17:56:12 -05:00
Lucas Smith 0d8532ab6d Merge pull request #293 from documenso/feat/refactor-shared-components
refactor: extract common components into UI package
2023-08-23 14:08:23 +10:00
Lucas Smith 490d3d51e1 Merge pull request #290 from nsylke/nsylke-patch-5
feat: create robots.txt and sitemap.xml
2023-08-23 13:56:46 +10:00
David Nguyen 2ab796910e refactor: extract common components into UI package 2023-08-23 11:22:13 +10:00
Mythie 07102588be chore: resolve build errors 2023-08-23 10:57:31 +10:00
Nicholas Sylke 04f9422f24 feat: robots.txt & sitemap.xml 2023-08-21 21:41:19 -05:00
Lucas Smith 05a7f5e178 Merge pull request #289 from adithyaakrishna/fix/codeql
fix: fixed build command in codeql workflow
2023-08-22 12:10:45 +10:00
Adithya Krishna 7bae814f96 fix: fixed build command in codeql workflow
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-22 07:26:26 +05:30
Lucas Smith ea45e38fa0 Merge pull request #273 from documenso/feat/feature-flag
feat: feature flags
2023-08-22 10:13:51 +10:00
Lucas Smith 6d9a85112f Merge pull request #286 from documenso/feat/blog-post-next
feat: add blog post 'Preview the next Documenso'
2023-08-22 10:07:43 +10:00
Lucas Smith 346efd19db Merge pull request #284 from adithyaakrishna/feat/general-updates
feat: added codeql and dependabot workflows
2023-08-22 10:06:39 +10:00
Adithya Krishna 617143a47f fix: removed ts from codeql
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-21 22:42:45 +05:30
flō 66f067276e Add visual asset for blog post 2023-08-21 17:24:29 +02:00
flō fc10d0449f Create blog post 2023-08-21 17:23:47 +02:00
Adithya Krishna 083f3e7108 feat: add codeql-analysis
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-21 19:08:47 +05:30
Adithya Krishna af307a2a49 chore: updated dependabot config
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-21 19:03:53 +05:30
Adithya Krishna b063758ee5 chore: update readme file with radix-ui
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-21 19:03:26 +05:30
Adithya Krishna 4964b252e3 chore: update readme file
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-21 18:56:48 +05:30
Adithya Krishna e468f5bbc9 chore: enable job concurrency
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-21 18:55:53 +05:30
Adithya Krishna c5b7b8a18a feat: add dependabot config
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-21 18:54:59 +05:30
Adithya Krishna a8a1fbb829 feat: Added Engines to Enforce Node v18.0.0 and above
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-21 18:54:44 +05:30
Timur Ercan 3c2a4892e7 Merge pull request #283 from documenso/chore/shop
chore: add shop to footer
2023-08-21 14:18:18 +02:00
Timur Ercan 6d9e84d327 chore: add shop to footer 2023-08-21 14:16:19 +02:00
Timur Ercan 73b4e30c97 Merge pull request #257 from documenso/feat/onepage
Feat/onepage
2023-08-21 13:33:04 +02:00
Lucas Smith bd01545a70 Merge pull request #271 from premiare/feat/mobileNavigation
feat: Mobile Navigation
2023-08-21 20:01:08 +10:00
Mythie 6d360e581d fix: styling updates 2023-08-21 19:56:18 +10:00
premiare 2f2d5dfc0b Merge branch 'feat/refresh' into feat/mobileNavigation 2023-08-18 19:57:26 +10:00
premiare 0f27f4261b add PORT number back to package.json 2023-08-18 19:37:07 +10:00
premiare 9b92cad2db move menu links to map, add window-size hook 2023-08-18 19:35:45 +10:00
premiare ad1ff6159c add reducedMotion check, center menu items 2023-08-18 19:05:46 +10:00
Lucas Smith 034072f50e Merge branch 'feat/refresh' into feat/onepage 2023-08-18 12:15:36 +10:00
premiare 45d0d3f7e8 extract hamburger to own file 2023-08-18 00:18:25 +10:00
premiare 6e62eb8d81 init mobileNavigation and added documenso symbol 2023-08-18 00:12:13 +10:00
Timur Ercan 3b9c57fe5c chore: remove double emtpy state add CTA 2023-08-16 13:49:19 +02:00
Timur Ercan 90e28cd3a4 chore: status order to figma 2023-08-16 09:29:16 +02:00
Timur Ercan e743e56787 remove dashboard and link bar 2023-08-16 09:03:28 +02:00
121 changed files with 114985 additions and 1378 deletions
+2
View File
@@ -12,6 +12,8 @@ NEXT_PUBLIC_APP_URL="http://localhost:3000"
# [[DATABASE]]
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]]
# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels
+7
View File
@@ -0,0 +1,7 @@
# Config files
*.config.js
*.config.cjs
# Statically hosted javascript files
apps/*/public/*.js
apps/*/public/*.cjs
+32
View 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
View File
@@ -6,6 +6,10 @@ on:
pull_request:
branches: [ "feat/refresh" ]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
HUSKY: 0
+45
View 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
@@ -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 }}
+4
View File
@@ -89,6 +89,10 @@ Documenso is built using awesome open source tech including:
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
- [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
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
+56
View 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 -2
View File
@@ -4,10 +4,11 @@
"private": true,
"license": "AGPL-3.0",
"scripts": {
"dev": "PORT=3001 next dev",
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
"@documenso/lib": "*",
@@ -29,6 +30,7 @@
"react-hook-form": "^7.43.9",
"react-icons": "^4.8.0",
"recharts": "^2.7.2",
"sharp": "0.32.5",
"typescript": "5.1.6",
"zod": "^3.21.4"
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

File diff suppressed because it is too large Load Diff
@@ -5,7 +5,7 @@ import { allDocuments } from 'contentlayer/generated';
import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks';
export const generateStaticParams = async () =>
export const generateStaticParams = () =>
allDocuments.map((post) => ({ post: post._raw.flattenedPath }));
export const generateMetadata = ({ params }: { params: { content: string } }) => {
@@ -7,7 +7,7 @@ import { ChevronLeft } from 'lucide-react';
import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks';
export const generateStaticParams = async () =>
export const generateStaticParams = () =>
allBlogPosts.map((post) => ({ post: post._raw.flattenedPath }));
export const generateMetadata = ({ params }: { params: { post: string } }) => {
@@ -8,7 +8,7 @@ import { FundingRaised } from './funding-raised';
import { GithubMetric } from './gh-metrics';
import { TeamMembers } from './team-members';
export const revalidate = 86400;
export const revalidate = 3600;
const ZGithubStatsResponse = z.object({
stargazers_count: z.number(),
@@ -43,7 +43,7 @@ export default async function OpenPage() {
accept: 'application/vnd.github.v3+json',
},
})
.then((res) => res.json())
.then(async (res) => res.json())
.then((res) => ZGithubStatsResponse.parse(res));
const { total_count: mergedPullRequests } = await fetch(
@@ -54,7 +54,7 @@ export default async function OpenPage() {
},
},
)
.then((res) => res.json())
.then(async (res) => res.json())
.then((res) => ZMergedPullRequestsResponse.parse(res));
const STARGAZERS_DATA = await fetch('https://stargrazer-live.onrender.com/api/stats', {
@@ -62,7 +62,7 @@ export default async function OpenPage() {
accept: 'application/json',
},
})
.then((res) => res.json())
.then(async (res) => res.json())
.then((res) => ZStargazersLiveResponse.parse(res));
return (
+1 -1
View File
@@ -24,7 +24,7 @@ export default async function IndexPage() {
accept: 'application/vnd.github.v3+json',
},
})
.then((res) => res.json())
.then(async (res) => res.json())
.then((res) => (typeof res.stargazers_count === 'number' ? res.stargazers_count : undefined))
.catch(() => undefined);
+14
View 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
View 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 { zodResolver } from '@hookform/resolvers/zod';
import { Info, Loader } from 'lucide-react';
import { Info } from 'lucide-react';
import { usePlausible } from 'next-plausible';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@@ -63,7 +63,9 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => {
try {
const delay = new Promise<void>((resolve) => setTimeout(resolve, 1000));
const delay = new Promise<void>((resolve) => {
setTimeout(resolve, 1000);
});
const [redirectUrl] = await Promise.all([
claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }),
@@ -85,7 +87,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
@@ -97,50 +99,49 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
</DialogDescription>
</DialogHeader>
<form
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">
<div className="flex">
<div className="flex-shrink-0">
<Info className="h-5 w-5 text-yellow-400" />
</div>
<div className="ml-3">
<p className="text-sm leading-5 text-yellow-700">
You have cancelled the payment process. If you didn't mean to do this, please
try again.
</p>
<form onSubmit={handleSubmit(onFormSubmit)}>
<fieldset disabled={isSubmitting} className={cn('flex flex-col gap-y-4', className)}>
{params?.get('cancelled') === 'true' && (
<div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<Info className="h-5 w-5 text-yellow-400" />
</div>
<div className="ml-3">
<p className="text-sm leading-5 text-yellow-700">
You have cancelled the payment process. If you didn't mean to do this, please
try again.
</p>
</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>
<Label className="text-slate-500">Name</Label>
<div>
<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} />
</div>
<FormErrorMessage className="mt-1" error={errors.email} />
</div>
<div>
<Label className="text-slate-500">Email</Label>
<Input type="email" className="mt-2" {...register('email')} />
<FormErrorMessage className="mt-1" error={errors.email} />
</div>
<Button type="submit" size="lg" disabled={isSubmitting}>
{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>
<Button type="submit" size="lg" loading={isSubmitting}>
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>
</fieldset>
</form>
</DialogContent>
</Dialog>
@@ -3,7 +3,7 @@ import { HTMLAttributes } from 'react';
import Image from 'next/image';
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';
@@ -36,11 +36,11 @@ export const Footer = ({ className, ...props }: FooterProps) => {
</Link>
<Link
href="https://documenso.slack.com"
href="https://documen.so/discord"
target="_blank"
className="hover:text-[#6D6D6D]"
>
<Slack className="h-6 w-6" />
<MessagesSquare className="h-6 w-6" />
</Link>
</div>
</div>
@@ -61,6 +61,14 @@ export const Footer = ({ className, ...props }: FooterProps) => {
Open
</Link>
<Link
href="https://shop.documenso.com"
target="_blank"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Shop
</Link>
<Link
href="https://status.documenso.com"
target="_blank"
@@ -1,20 +1,27 @@
import { HTMLAttributes } from 'react';
'use client';
import { HTMLAttributes, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { cn } from '@documenso/ui/lib/utils';
import { HamburgerMenu } from './mobile-hamburger';
import { MobileNavigation } from './mobile-navigation';
export type HeaderProps = HTMLAttributes<HTMLElement>;
export const Header = ({ className, ...props }: HeaderProps) => {
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
return (
<header className={cn('flex items-center justify-between', className)} {...props}>
<Link href="/">
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} />
</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]">
Pricing
</Link>
@@ -35,6 +42,15 @@ export const Header = ({ className, ...props }: HeaderProps) => {
Sign in
</Link>
</div>
<HamburgerMenu
onToggleMenuOpen={() => setIsHamburgerMenuOpen((v) => !v)}
isMenuOpen={isHamburgerMenuOpen}
/>
<MobileNavigation
isMenuOpen={isHamburgerMenuOpen}
onMenuOpenChange={setIsHamburgerMenuOpen}
/>
</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>
);
};
@@ -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>
);
};
@@ -13,7 +13,7 @@ export const PasswordReveal = ({ password }: PasswordRevealProps) => {
const [, copy] = useCopyToClipboard();
const onCopyClick = () => {
copy(password).then(() => {
void copy(password).then(() => {
toast({
title: 'Copied to clipboard',
description: 'Your password has been copied to your clipboard.',
@@ -21,12 +21,12 @@ import {
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { claimPlan } from '~/api/claim-plan/fetcher';
import { FormErrorMessage } from '../form/form-error-message';
import { SignaturePad } from '../signature-pad';
const ZWidgetFormSchema = z
.object({
@@ -124,7 +124,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
setValue('signatureDataUrl', draftSignatureDataUrl);
setValue('signatureText', '');
trigger('signatureDataUrl');
void trigger('signatureDataUrl');
setShowSigningDialog(false);
};
@@ -135,7 +135,9 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
signatureText,
}: TWidgetFormSchema) => {
try {
const delay = new Promise<void>((resolve) => setTimeout(resolve, 1000));
const delay = new Promise<void>((resolve) => {
setTimeout(resolve, 1000);
});
// eslint-disable-next-line turbo/no-undeclared-env-vars
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
@@ -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>
);
};
@@ -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;
}
+4 -12
View File
@@ -9,16 +9,10 @@
}
],
"paths": {
"~/*": [
"./src/*"
],
"contentlayer/generated": [
"./.contentlayer/generated"
]
"~/*": ["./src/*"],
"contentlayer/generated": ["./.contentlayer/generated"]
},
"types": [
"@documenso/lib/types/next-auth.d.ts"
],
"types": ["@documenso/lib/types/next-auth.d.ts"],
"strictNullChecks": true,
"incremental": false
},
@@ -29,7 +23,5 @@
".next/types/**/*.ts",
".contentlayer/generated"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}
+4 -3
View File
@@ -4,10 +4,11 @@
"private": true,
"license": "AGPL-3.0",
"scripts": {
"dev": "PORT=3000 next dev",
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
"@documenso/lib": "*",
@@ -34,8 +35,8 @@
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.43.9",
"react-icons": "^4.8.0",
"react-pdf": "^7.1.1",
"react-rnd": "^10.4.1",
"sharp": "0.32.5",
"ts-pattern": "^5.0.5",
"typescript": "5.1.6",
"zod": "^3.21.4"
+56611
View File
File diff suppressed because it is too large Load Diff
@@ -5,10 +5,10 @@ import { useRouter } from 'next/navigation';
import { Loader } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCreateDocument } from '~/api/document/create/fetcher';
import { DocumentDropzone } from '~/components/(dashboard)/document-dropzone/document-dropzone';
export type UploadDocumentProps = {
className?: string;
@@ -2,14 +2,28 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Document, Field, Recipient, User } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
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 { AddFieldsFormPartial } from '~/components/forms/edit-document/add-fields';
import { AddSignersFormPartial } from '~/components/forms/edit-document/add-signers';
import { AddSubjectFormPartial } from '~/components/forms/edit-document/add-subject';
import { addFields } from '~/components/forms/edit-document/add-fields.action';
import { addSigners } from '~/components/forms/edit-document/add-signers.action';
import { completeDocument } from '~/components/forms/edit-document/add-subject.action';
export type EditDocumentFormProps = {
className?: string;
@@ -19,6 +33,8 @@ export type EditDocumentFormProps = {
fields: Field[];
};
type EditDocumentStep = 'signers' | 'fields' | 'subject';
export const EditDocumentForm = ({
className,
document,
@@ -26,27 +42,103 @@ export const EditDocumentForm = ({
fields,
user: _user,
}: 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 onNextStep = () => {
if (step === 'signers') {
setStep('fields');
}
const documentFlow: Record<EditDocumentStep, DocumentFlowStep> = {
signers: {
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') {
setStep('subject');
const currentDocumentFlow = documentFlow[step];
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 = () => {
if (step === 'fields') {
setStep('signers');
}
const onAddFieldsFormSubmit = async (data: TAddFieldsFormSchema) => {
try {
// Custom invocation server action
await addFields({
documentId: document.id,
fields: data.fields,
});
if (step === 'subject') {
setStep('fields');
router.refresh();
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>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
{step === 'signers' && (
<AddSignersFormPartial
recipients={recipients}
fields={fields}
document={document}
onContinue={onNextStep}
onGoBack={onPreviousStep}
<DocumentFlowFormContainer onSubmit={(e) => e.preventDefault()}>
<DocumentFlowFormContainerHeader
title={currentDocumentFlow.title}
description={currentDocumentFlow.description}
/>
)}
{step === 'fields' && (
<AddFieldsFormPartial
recipients={recipients}
fields={fields}
document={document}
onContinue={onNextStep}
onGoBack={onPreviousStep}
/>
)}
{step === 'signers' && (
<AddSignersFormPartial
documentFlow={documentFlow.signers}
recipients={recipients}
fields={fields}
numberOfSteps={Object.keys(documentFlow).length}
onSubmit={onAddSignersFormSubmit}
/>
)}
{step === 'subject' && (
<AddSubjectFormPartial
recipients={recipients}
fields={fields}
document={document}
onContinue={onNextStep}
onGoBack={onPreviousStep}
/>
)}
{step === 'fields' && (
<AddFieldsFormPartial
documentFlow={documentFlow.fields}
recipients={recipients}
fields={fields}
numberOfSteps={Object.keys(documentFlow).length}
onSubmit={onAddFieldsFormSubmit}
/>
)}
{step === 'subject' && (
<AddSubjectFormPartial
documentFlow={documentFlow.subject}
document={document}
recipients={recipients}
fields={fields}
numberOfSteps={Object.keys(documentFlow).length}
onSubmit={onAddSubjectFormSubmit}
/>
)}
</DocumentFlowFormContainer>
</div>
</div>
);
@@ -1,34 +1,19 @@
'use client';
import dynamic from 'next/dynamic';
import { Loader } from 'lucide-react';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewerProps } from '~/components/(dashboard)/pdf-viewer/pdf-viewer';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { PDFViewerProps } from '@documenso/ui/primitives/pdf-viewer';
export type LoadablePDFCard = PDFViewerProps & {
className?: 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) => {
return (
<Card className={className} gradient {...props}>
<CardContent className="p-2">
<PDFViewer className={pdfClassName} {...props} />
<LazyPDFViewer className={pdfClassName} {...props} />
</CardContent>
</Card>
);
@@ -9,12 +9,13 @@ export default function Loading() {
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
Documents
</Link>
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
Loading Document...
</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="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" />
<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 { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-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 { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { DocumentStatus } from '~/components/formatter/document-status';
export type DocumentPageProps = {
@@ -69,18 +72,28 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
<div className="text-muted-foreground flex items-center">
<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>
<EditDocumentForm
className="mt-8"
document={document}
user={session}
recipients={recipients}
fields={fields}
/>
{document.status !== InternalDocumentStatus.COMPLETED && (
<EditDocumentForm
className="mt-8"
document={document}
user={session}
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>
);
}
+24 -38
View File
@@ -1,23 +1,17 @@
import Link from 'next/link';
import { Plus } from 'lucide-react';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import { isDocumentStatus } from '@documenso/lib/types/is-document-status';
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
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 {
PeriodSelectorValue,
isPeriodSelectorValue,
} from '~/components/(dashboard)/period-selector/types';
import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
import { DocumentStatus } from '~/components/formatter/document-status';
import { UploadDocument } from '../dashboard/upload-document';
import { DocumentsDataTable } from './data-table';
export type DocumentsPageProps = {
@@ -37,10 +31,12 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
});
const status = isDocumentStatus(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 perPage = Number(searchParams.perPage) || 20;
const shouldDefaultToPending = status === 'ALL' && stats.PENDING > 0;
const results = await findDocuments({
userId: session.id,
status: status === 'ALL' ? undefined : status,
@@ -52,8 +48,6 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
perPage,
});
const isNoResults = status === 'ALL' && period === '' && results.data.length === 0;
const getTabHref = (value: typeof status) => {
const params = new URLSearchParams(searchParams);
@@ -72,25 +66,13 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
return (
<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">
<Tabs defaultValue={status}>
<Tabs defaultValue={shouldDefaultToPending ? InternalDocumentStatus.PENDING : status}>
<TabsList>
<TabsTrigger className="min-w-[60px]" value="ALL" asChild>
<Link href={getTabHref('ALL')}>All</Link>
</TabsTrigger>
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.DRAFT} asChild>
<Link href={getTabHref(InternalDocumentStatus.DRAFT)}>
<DocumentStatus status={InternalDocumentStatus.DRAFT} />
<span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats.DRAFT, 99)}
</span>
</Link>
</TabsTrigger>
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.PENDING} asChild>
<Link href={getTabHref(InternalDocumentStatus.PENDING)}>
<DocumentStatus status={InternalDocumentStatus.PENDING} />
@@ -110,26 +92,30 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
</span>
</Link>
</TabsTrigger>
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.DRAFT} asChild>
<Link href={getTabHref(InternalDocumentStatus.DRAFT)}>
<DocumentStatus status={InternalDocumentStatus.DRAFT} />
<span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats.DRAFT, 99)}
</span>
</Link>
</TabsTrigger>
<TabsTrigger className="min-w-[60px]" value="ALL" asChild>
<Link href={getTabHref('ALL')}>All</Link>
</TabsTrigger>
</TabsList>
</Tabs>
<div className="flex flex-1 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<PeriodSelector />
<Button>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add Document
</Button>
</div>
</div>
<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 */}
{isNoResults ? (
<DocumentDropzone className="min-h-[60vh] md:min-h-[40vh]" />
) : (
<DocumentsDataTable results={results} />
)}
<DocumentsDataTable results={results} />
</div>
</div>
);
@@ -12,8 +12,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '~/components/signature-pad';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useRequiredSigningContext } from './provider';
@@ -51,7 +50,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
return (
<form
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)}
>
@@ -65,7 +64,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
<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>
<Label htmlFor="full-name">Full Name</Label>
@@ -99,10 +98,10 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
</div>
</div>
<div className="flex gap-4">
<div className="flex flex-col gap-4 md:flex-row">
<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"
size="lg"
>
@@ -110,8 +109,8 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
</Button>
<Button
className="w-full"
type="submit"
className="flex-1"
size="lg"
disabled={!isComplete || isSubmitting}
>
@@ -149,7 +149,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => {
disabled={!localFullName}
onClick={() => {
setShowFullNameModal(false);
onSign('local');
void onSign('local');
}}
>
Sign
@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation';
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 { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
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 { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { LazyPDFViewer } from '~/components/(dashboard)/pdf-viewer/lazy-pdf-viewer';
import { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { DateField } from './date-field';
import { SigningForm } from './form';
@@ -58,7 +57,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
</p>
</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
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
gradient
@@ -12,10 +12,9 @@ import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { SignaturePad } from '~/components/signature-pad';
import { useRequiredSigningContext } from './provider';
import { SigningFieldContainer } from './signing-field-container';
@@ -183,7 +182,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
disabled={!localSignature}
onClick={() => {
setShowSignatureModal(false);
onSign('local');
void onSign('local');
}}
>
Sign
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

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={`
${zIndexClass}
${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>
</Avatar>
@@ -11,7 +11,17 @@ import {
import { StackAvatar } from './stack-avatar';
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(
(recipient) => getRecipientType(recipient) === 'waiting',
);
@@ -32,9 +42,10 @@ export const StackAvatarsWithTooltip = ({ recipients }: { recipients: Recipient[
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="flex cursor-pointer">
<StackAvatars recipients={recipients} />
{children || <StackAvatars recipients={recipients} />}
</TooltipTrigger>
<TooltipContent>
<TooltipContent side={position}>
<div className="flex flex-col gap-y-5 p-1">
{completedRecipients.length > 0 && (
<div>
@@ -2,30 +2,15 @@
import { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@documenso/ui/lib/utils';
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
const pathname = usePathname();
return (
<div className={cn('ml-8 hidden flex-1 gap-x-6 md:flex', className)} {...props}>
<Link
href="/dashboard"
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
{/* No Nav tabs while there is only one main page */}
{/* <Link
href="/documents"
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 ',
@@ -35,14 +20,6 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
)}
>
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> */}
</div>
);
@@ -118,7 +118,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
<DropdownMenuItem
onSelect={() =>
signOut({
void signOut({
callbackUrl: '/',
})
}
@@ -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>
),
},
);
@@ -63,7 +63,9 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => {
try {
const delay = new Promise<void>((resolve) => setTimeout(resolve, 1000));
const delay = new Promise<void>((resolve) => {
setTimeout(resolve, 1000);
});
const [redirectUrl] = await Promise.all([
claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }),
@@ -13,7 +13,7 @@ export const PasswordReveal = ({ password }: PasswordRevealProps) => {
const [, copy] = useCopyToClipboard();
const onCopyClick = () => {
copy(password).then(() => {
void copy(password).then(() => {
toast({
title: 'Copied to clipboard',
description: 'Your password has been copied to your clipboard.',
@@ -21,12 +21,12 @@ import {
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { claimPlan } from '~/api/claim-plan/fetcher';
import { FormErrorMessage } from '../form/form-error-message';
import { SignaturePad } from '../signature-pad';
const ZWidgetFormSchema = z
.object({
@@ -124,7 +124,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
setValue('signatureDataUrl', draftSignatureDataUrl);
setValue('signatureText', '');
trigger('signatureDataUrl');
void trigger('signatureDataUrl');
setShowSigningDialog(false);
};
@@ -135,7 +135,9 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
signatureText,
}: TWidgetFormSchema) => {
try {
const delay = new Promise<void>((resolve) => setTimeout(resolve, 1000));
const delay = new Promise<void>((resolve) => {
setTimeout(resolve, 1000);
});
// eslint-disable-next-line turbo/no-undeclared-env-vars
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
@@ -13,11 +13,6 @@ type FriendlyStatus = {
};
const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
DRAFT: {
label: 'Draft',
icon: File,
color: 'text-yellow-500',
},
PENDING: {
label: 'Pending',
icon: Clock,
@@ -28,6 +23,11 @@ const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
icon: CheckCircle2,
color: 'text-green-500',
},
DRAFT: {
label: 'Draft',
icon: File,
color: 'text-yellow-500',
},
};
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
@@ -2,8 +2,7 @@
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
import { TAddFieldsFormSchema } from './add-fields.types';
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
export type AddFieldsActionInput = TAddFieldsFormSchema & {
documentId: number;
@@ -2,8 +2,7 @@
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { TAddSignersFormSchema } from './add-signers.types';
import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
export type AddSignersActionInput = TAddSignersFormSchema & {
documentId: number;
@@ -2,8 +2,7 @@
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { TAddSubjectFormSchema } from './add-subject.types';
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
documentId: number;
+8 -2
View File
@@ -18,8 +18,8 @@ import { FormErrorMessage } from '../form/form-error-message';
export const ZPasswordFormSchema = z
.object({
password: z.string().min(6),
repeatedPassword: z.string().min(6),
password: z.string().min(6).max(72),
repeatedPassword: z.string().min(6).max(72),
})
.refine((data) => data.password === data.repeatedPassword, {
message: 'Passwords do not match',
@@ -92,6 +92,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
<Input
id="password"
type="password"
minLength={6}
maxLength={72}
autoComplete="new-password"
className="bg-background mt-2"
{...register('password')}
/>
@@ -107,6 +110,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
<Input
id="repeated-password"
type="password"
minLength={6}
maxLength={72}
autoComplete="new-password"
className="bg-background mt-2"
{...register('repeatedPassword')}
/>
+1 -1
View File
@@ -14,10 +14,10 @@ import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { FormErrorMessage } from '../form/form-error-message';
import { SignaturePad } from '../signature-pad';
export const ZProfileFormSchema = z.object({
name: z.string().min(1),
+6 -6
View File
@@ -15,7 +15,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZSignInFormSchema = z.object({
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>;
@@ -44,7 +44,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
await signIn('credentials', {
email,
password,
callbackUrl: '/dashboard',
callbackUrl: '/documents',
}).catch((err) => {
console.error(err);
});
@@ -76,10 +76,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
return (
<form
className={cn('flex w-full flex-col gap-y-4', className)}
onSubmit={(e) => {
e.preventDefault();
handleSubmit(onFormSubmit)();
}}
onSubmit={handleSubmit(onFormSubmit)}
>
<div>
<Label htmlFor="email" className="text-slate-500">
@@ -99,6 +96,9 @@ export const SignInForm = ({ className }: SignInFormProps) => {
<Input
id="password"
type="password"
minLength={6}
maxLength={72}
autoComplete="current-password"
className="bg-background mt-2"
{...register('password')}
/>
+5 -3
View File
@@ -12,14 +12,13 @@ import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { SignaturePad } from '../signature-pad';
export const ZSignUpFormSchema = z.object({
name: z.string().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>;
@@ -106,6 +105,9 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
<Input
id="password"
type="password"
minLength={6}
maxLength={72}
autoComplete="new-password"
className="bg-background mt-2"
{...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);
}
}
+2 -2
View File
@@ -32,7 +32,7 @@ export const getFlag = async (
revalidate: 60,
},
})
.then((res) => res.json())
.then(async (res) => res.json())
.then((res) => ZFeatureFlagValueSchema.parse(res))
.catch(() => false);
@@ -64,7 +64,7 @@ export const getAllFlags = async (
revalidate: 60,
},
})
.then((res) => res.json())
.then(async (res) => res.json())
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
.catch(() => LOCAL_FEATURE_FLAGS);
};
@@ -11,6 +11,6 @@ export default function PostHogServerClient() {
return new PostHog(postHogConfig.key, {
host: postHogConfig.host,
fetch: (...args) => fetch(...args),
fetch: async (...args) => fetch(...args),
});
}
+2 -3
View File
@@ -1,10 +1,9 @@
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 { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
import { getBoundingClientRect } from '~/helpers/get-bounding-client-rect';
export const useFieldPageCoords = (field: Field) => {
const [coords, setCoords] = useState({
x: 0,
+2 -2
View File
@@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
export default async function middleware(req: NextRequest) {
export default function middleware(req: NextRequest) {
if (req.nextUrl.pathname === '/') {
const redirectUrl = new URL('/dashboard', req.url);
const redirectUrl = new URL('/documents', req.url);
return NextResponse.redirect(redirectUrl);
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { appRouter } from '@documenso/trpc/server/router';
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: ({ req, res }) => createTrpcContext({ req, res }),
createContext: async ({ req, res }) => createTrpcContext({ req, res }),
});
// export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
+2 -2
View File
@@ -67,7 +67,7 @@ export function FeatureFlagProvider({
const interval = setInterval(() => {
if (document.hasFocus()) {
getAllFlags().then((newFlags) => setFlags(newFlags));
void getAllFlags().then((newFlags) => setFlags(newFlags));
}
}, FEATURE_FLAG_POLL_INTERVAL);
@@ -84,7 +84,7 @@ export function FeatureFlagProvider({
return;
}
const onFocus = () => getAllFlags().then((newFlags) => setFlags(newFlags));
const onFocus = () => void getAllFlags().then((newFlags) => setFlags(newFlags));
window.addEventListener('focus', onFocus);
+4 -12
View File
@@ -9,16 +9,10 @@
}
],
"paths": {
"~/*": [
"./src/*"
],
"contentlayer/generated": [
"./.contentlayer/generated"
]
"~/*": ["./src/*"],
"contentlayer/generated": ["./.contentlayer/generated"]
},
"types": [
"@documenso/lib/types/next-auth.d.ts"
],
"types": ["@documenso/lib/types/next-auth.d.ts"],
"strictNullChecks": true,
"incremental": false
},
@@ -30,7 +24,5 @@
".next/types/**/*.ts",
".contentlayer/generated"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}
+440 -6
View File
@@ -23,6 +23,10 @@
"lint-staged": "^14.0.0",
"prettier": "^2.5.1",
"turbo": "^1.9.3"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.6.0"
}
},
"apps/marketing": {
@@ -49,6 +53,7 @@
"react-hook-form": "^7.43.9",
"react-icons": "^4.8.0",
"recharts": "^2.7.2",
"sharp": "0.32.5",
"typescript": "5.1.6",
"zod": "^3.21.4"
},
@@ -87,8 +92,8 @@
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.43.9",
"react-icons": "^4.8.0",
"react-pdf": "^7.1.1",
"react-rnd": "^10.4.1",
"sharp": "0.32.5",
"ts-pattern": "^5.0.5",
"typescript": "5.1.6",
"zod": "^3.21.4"
@@ -5210,6 +5215,11 @@
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -5224,6 +5234,25 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"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": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz",
@@ -5253,6 +5282,16 @@
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
@@ -5315,6 +5354,29 @@
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -5991,6 +6053,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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -6007,6 +6081,15 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"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": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
@@ -6541,6 +6624,14 @@
"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": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -6894,6 +6985,14 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"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": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
@@ -7971,6 +8070,14 @@
"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": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -8005,6 +8112,11 @@
"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": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
@@ -8255,6 +8367,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": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
@@ -8465,6 +8582,11 @@
"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": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
@@ -8983,6 +9105,25 @@
"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": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -11364,6 +11505,11 @@
"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": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
@@ -11415,6 +11561,11 @@
"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": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -11615,6 +11766,17 @@
"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": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
@@ -12443,6 +12605,111 @@
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -12657,6 +12924,15 @@
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"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": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@@ -12698,6 +12974,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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
@@ -12740,6 +13021,28 @@
"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": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz",
@@ -12851,9 +13154,9 @@
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-pdf": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.0.tgz",
"integrity": "sha512-FRUqxGqh/l9TfND7T9ooW5ulZZt1tPYVwSEonFv3ImQFp3OkPM09BljchwEGHszP7P6h/Tt8js6v6d/T7GTC3A==",
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.3.tgz",
"integrity": "sha512-d7WAxcsjOogJfJ+I+zX/mdip3VjR1yq/yDa4hax4XbQVjbbbup6rqs4c8MGx0MLSnzob17TKp1t4CsNbDZ6GeQ==",
"dependencies": {
"clsx": "^2.0.0",
"make-cancellable-promise": "^1.3.1",
@@ -13792,6 +14095,82 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -13846,8 +14225,7 @@
"type": "consulting",
"url": "https://feross.org/support"
}
],
"optional": true
]
},
"node_modules/simple-get": {
"version": "3.1.1",
@@ -13860,6 +14238,19 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -14006,6 +14397,15 @@
"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": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -14469,6 +14869,26 @@
"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": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz",
@@ -14910,6 +15330,17 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"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": {
"version": "1.10.12",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.12.tgz",
@@ -16197,7 +16628,10 @@
"date-fns": "^2.30.0",
"framer-motion": "^10.12.8",
"lucide-react": "^0.214.0",
"next": "13.4.12",
"pdfjs-dist": "3.6.172",
"react-day-picker": "^8.7.1",
"react-pdf": "^7.3.3",
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5"
},
+4
View File
@@ -9,6 +9,10 @@
"prepare": "husky install",
"commitlint": "commitlint --edit"
},
"engines": {
"npm": ">=8.6.0",
"node": ">=18.0.0"
},
"devDependencies": {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
+4 -3
View File
@@ -110,9 +110,10 @@ export class MailChannelsTransport implements Transport<SentMessageInfo> {
});
}
res.json().then((data) => {
return callback(new Error(`MailChannels error: ${data.message}`), null);
});
res
.json()
.then((data) => callback(new Error(`MailChannels error: ${data.message}`), null))
.catch((err) => callback(err, null));
})
.catch((err) => {
return callback(err, null);
+14
View File
@@ -19,6 +19,8 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['../../apps/*/tsconfig.json', '../../packages/*/tsconfig.json'],
ecmaVersion: 2022,
ecmaFeatures: {
jsx: true,
@@ -32,6 +34,18 @@ module.exports = {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
// Safety with promises so we aren't running with scissors
'no-promise-executor-return': 'error',
'prefer-promise-reject-errors': 'error',
'require-atomic-updates': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
{ checksVoidReturn: { attributes: false } },
],
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/require-await': 'error',
// We never want to use `as` but are required to on occasion to handle
// shortcomings in third-party and generated types.
//
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@documenso/tsconfig/base.json",
"compilerOptions": {
"allowJs": true,
"noEmit": true,
},
"include": ["**/*.cjs", "**/*.js"],
"exclude": ["dist", "build", "node_modules"]
}
@@ -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,
};
};
+10 -7
View File
@@ -7,6 +7,7 @@ import GoogleProvider, { GoogleProfile } from 'next-auth/providers/google';
import { prisma } from '@documenso/prisma';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
import { ErrorCodes } from './error-codes';
export const NEXT_AUTH_OPTIONS: AuthOptions = {
adapter: PrismaAdapter(prisma),
@@ -23,21 +24,23 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
},
authorize: async (credentials, _req) => {
if (!credentials) {
return null;
throw new Error(ErrorCodes.CredentialsNotFound);
}
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) {
return null;
if (!user.password) {
throw new Error(ErrorCodes.UserMissingPassword);
}
const isPasswordsSame = compare(password, user.password);
const isPasswordsSame = await compare(password, user.password);
if (!isPasswordsSame) {
return null;
throw new Error(ErrorCodes.IncorrectEmailPassword);
}
return {
@@ -86,7 +89,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
};
},
async session({ token, session }) {
session({ token, session }) {
if (token && token.email) {
return {
...session,
+5
View File
@@ -0,0 +1,5 @@
export const ErrorCodes = {
IncorrectEmailPassword: 'incorrect-email-password',
UserMissingPassword: 'missing-password',
CredentialsNotFound: 'credentials-not-found',
} as const;
@@ -1,9 +1,6 @@
import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next';
import { headers } from 'next/headers';
import { NextRequest } from 'next/server';
import { getServerSession as getNextAuthServerSession } from 'next-auth';
import { getToken } from 'next-auth/jwt';
import { prisma } from '@documenso/prisma';
@@ -30,18 +27,6 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
return user;
};
export const getServerComponentToken = async () => {
const requestHeaders = Object.fromEntries(headers().entries());
const req = new NextRequest('http://example.com', {
headers: requestHeaders,
});
const token = await getToken({
req,
});
};
export const getServerComponentSession = async () => {
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
@@ -87,6 +87,6 @@ export const completeDocumentWithToken = async ({
if (documents.count > 0) {
console.log('sealing document');
sealDocument({ documentId: document.id });
await sealDocument({ documentId: document.id });
}
};
@@ -59,7 +59,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
signDocumentLink,
});
mailer.sendMail({
await mailer.sendMail({
to: {
address: email,
name,
+3 -3
View File
@@ -1,11 +1,11 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["extendedWhereUnique", "jsonProtocol"]
}
datasource db {
provider = "postgresql"
url = env("NEXT_PRIVATE_DATABASE_URL")
provider = "postgresql"
url = env("NEXT_PRIVATE_DATABASE_URL")
directUrl = env("NEXT_PRIVATE_DIRECT_DATABASE_URL")
}
enum IdentityProvider {
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@documenso/tsconfig/base.json",
"compilerOptions": {
"allowJs": true,
"noEmit": true,
},
"include": ["**/*.cjs", "**/*.js"],
"exclude": ["dist", "build", "node_modules"]
}
+2 -2
View File
@@ -10,7 +10,7 @@ const t = initTRPC.context<TrpcContext>().create({
/**
* Middlewares
*/
export const authenticatedMiddleware = t.middleware(({ ctx, next }) => {
export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
@@ -18,7 +18,7 @@ export const authenticatedMiddleware = t.middleware(({ ctx, next }) => {
});
}
return next({
return await next({
ctx: {
...ctx,
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./base.json",
"compilerOptions": {
"noEmit": true,
},
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
"exclude": ["dist", "build", "node_modules"]
}
+4 -1
View File
@@ -54,8 +54,11 @@
"date-fns": "^2.30.0",
"framer-motion": "^10.12.8",
"lucide-react": "^0.214.0",
"next": "13.4.12",
"pdfjs-dist": "3.6.172",
"react-day-picker": "^8.7.1",
"react-pdf": "^7.3.3",
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5"
}
}
}
+37 -2
View File
@@ -2,6 +2,7 @@ import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { VariantProps, cva } from 'class-variance-authority';
import { Loader } from 'lucide-react';
import { cn } from '../lib/utils';
@@ -30,17 +31,51 @@ const buttonVariants = cva(
},
);
const loaderVariants = cva('mr-2 animate-spin', {
variants: {
size: {
default: 'h-5 w-5',
sm: 'h-4 w-4',
lg: 'h-5 w-5',
},
},
defaultVariants: {
size: 'default',
},
});
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
/**
* Will display the loading spinner and disable the button.
*/
loading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
if (asChild) {
return (
<Slot className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
}
const showLoader = props.loading === true;
const isDisabled = props.disabled || showLoader;
return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
disabled={isDisabled}
>
{showLoader && <Loader className={cn('mr-2 animate-spin', loaderVariants({ size }))} />}
{props.children}
</button>
);
},
);

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