Compare commits
32 Commits
feat/featu
...
feat/refac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab796910e | ||
|
|
07102588be | ||
|
|
05a7f5e178 | ||
|
|
7bae814f96 | ||
|
|
ea45e38fa0 | ||
|
|
6d9a85112f | ||
|
|
346efd19db | ||
|
|
617143a47f | ||
|
|
66f067276e | ||
|
|
fc10d0449f | ||
|
|
083f3e7108 | ||
|
|
af307a2a49 | ||
|
|
b063758ee5 | ||
|
|
4964b252e3 | ||
|
|
e468f5bbc9 | ||
|
|
c5b7b8a18a | ||
|
|
a8a1fbb829 | ||
|
|
3c2a4892e7 | ||
|
|
6d9e84d327 | ||
|
|
73b4e30c97 | ||
|
|
bd01545a70 | ||
|
|
6d360e581d | ||
|
|
2f2d5dfc0b | ||
|
|
0f27f4261b | ||
|
|
9b92cad2db | ||
|
|
ad1ff6159c | ||
|
|
034072f50e | ||
|
|
45d0d3f7e8 | ||
|
|
6e62eb8d81 | ||
|
|
3b9c57fe5c | ||
|
|
90e28cd3a4 | ||
|
|
e743e56787 |
39
.github/dependabot.yml
vendored
Normal file
39
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "feat/refresh" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "feat/refresh" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: 'github-actions'
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
target-branch: "feat/refresh"
|
||||||
|
labels:
|
||||||
|
- "ci dependencies"
|
||||||
|
- "ci"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/apps/marketing"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
target-branch: "feat/refresh"
|
||||||
|
labels:
|
||||||
|
- "npm dependencies"
|
||||||
|
- "frontend"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/apps/web"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
target-branch: "feat/refresh"
|
||||||
|
labels:
|
||||||
|
- "npm dependencies"
|
||||||
|
- "frontend"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -6,6 +6,10 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "feat/refresh" ]
|
branches: [ "feat/refresh" ]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
|
|
||||||
|
|||||||
45
.github/workflows/codeql-analysis.yml
vendored
Normal file
45
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [ feat/refresh ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ feat/refresh ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript' ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build Documenso
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
@@ -89,6 +89,10 @@ Documenso is built using awesome open source tech including:
|
|||||||
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
|
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
|
||||||
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
|
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
|
||||||
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
|
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
|
||||||
|
- [Zod for schema declaration and validation](https://zod.dev/)
|
||||||
|
- [Lucide React for icons in React app](https://lucide.dev/)
|
||||||
|
- [Framer Motion for motion library](https://www.framer.com/motion/)
|
||||||
|
- [Radix UI for component library](https://www.radix-ui.com/)
|
||||||
- Check out `/package.json` and `/apps/web/package.json` for more
|
- Check out `/package.json` and `/apps/web/package.json` for more
|
||||||
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
|
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
|
||||||
|
|
||||||
|
|||||||
56
apps/marketing/content/blog/next.mdx
Normal file
56
apps/marketing/content/blog/next.mdx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
title: Preview the next Documenso
|
||||||
|
description: We're redesigning Documenso by making it more elegant and appropriately playful. Here's a sneak peek.
|
||||||
|
authorName: 'Timur Ercan'
|
||||||
|
authorImage: '/blog/blog-author-timur.jpeg'
|
||||||
|
authorRole: 'Co-Founder'
|
||||||
|
date: 2023-08-21
|
||||||
|
tags:
|
||||||
|
- Design
|
||||||
|
- Preview
|
||||||
|
---
|
||||||
|
|
||||||
|
Since we launched [Documenso 0.9 on Product Hunt](https://producthunt.com/products/documenso#documenso) last May, the team's been hard at work behind the scenes to ramp up development and design to deliver an excellent next version.
|
||||||
|
|
||||||
|
Last week, Lucas shared the reasoning how [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite).
|
||||||
|
|
||||||
|
Today, I'm pleased to share with you a preview of the next Documenso.
|
||||||
|
|
||||||
|
## Preview the next Documenso
|
||||||
|
|
||||||
|
We redesigned the whole signing flow to make it more appealing and more convenient.
|
||||||
|
|
||||||
|
We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it.
|
||||||
|
|
||||||
|
**We call it happy minimalism.**
|
||||||
|
|
||||||
|
We paid particular attention to the moment of signing, which should be celebrated.
|
||||||
|
|
||||||
|
The image below is the final bloom of the completion celebration we added:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<MdxNextImage
|
||||||
|
src="/blog/blog-fig-preview-documenso.webp"
|
||||||
|
width="2000"
|
||||||
|
height="1268"
|
||||||
|
alt="Figure 1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<figcaption className="text-center">"You've signed a new document."</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## Kicking off a new phase of collaboration
|
||||||
|
|
||||||
|
This preview also is the kickoff for a new phase of how we collaborate with the community.
|
||||||
|
|
||||||
|
We recently [switched to Discord](https://documenso.com/blog/switching-from-slack-to-discord) to set up a more developer-friendly, community-driven environment, and we just released the [public roadmap](https://documen.so/launches).
|
||||||
|
|
||||||
|
As always, if you have any questions or feedback, please reach out. We love to hear from you.
|
||||||
|
|
||||||
|
Best from Hamburg,
|
||||||
|
|
||||||
|
Timur
|
||||||
|
|
||||||
|
Make sure to [star the GitHub repository](https://documen.so/github), [follow us on X](http://documen.so/twitter) and [join the Discord server](https://documen.so/discord) to keep up to date with all things Documenso.
|
||||||
|
|
||||||
|
We're building a beautiful, open-source alternative to DocuSign.
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
"dev": "PORT=3001 next dev",
|
"dev": "PORT=3001 next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
|
|||||||
BIN
apps/marketing/public/blog/blog-fig-preview-documenso.webp
Normal file
BIN
apps/marketing/public/blog/blog-fig-preview-documenso.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
BIN
apps/marketing/public/logo_icon.png
Normal file
BIN
apps/marketing/public/logo_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
56611
apps/marketing/public/pdf.worker.min.js
vendored
Normal file
56611
apps/marketing/public/pdf.worker.min.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import { HTMLAttributes } from 'react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Github, Slack, Twitter } from 'lucide-react';
|
import { Github, MessagesSquare, Twitter } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
@@ -36,11 +36,11 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="https://documenso.slack.com"
|
href="https://documen.so/discord"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="hover:text-[#6D6D6D]"
|
className="hover:text-[#6D6D6D]"
|
||||||
>
|
>
|
||||||
<Slack className="h-6 w-6" />
|
<MessagesSquare className="h-6 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,6 +61,14 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
|||||||
Open
|
Open
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://shop.documenso.com"
|
||||||
|
target="_blank"
|
||||||
|
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
|
>
|
||||||
|
Shop
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="https://status.documenso.com"
|
href="https://status.documenso.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
'use client';
|
||||||
|
|
||||||
|
import { HTMLAttributes, useState } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
import { HamburgerMenu } from './mobile-hamburger';
|
||||||
|
import { MobileNavigation } from './mobile-navigation';
|
||||||
|
|
||||||
export type HeaderProps = HTMLAttributes<HTMLElement>;
|
export type HeaderProps = HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
export const Header = ({ className, ...props }: HeaderProps) => {
|
export const Header = ({ className, ...props }: HeaderProps) => {
|
||||||
|
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={cn('flex items-center justify-between', className)} {...props}>
|
<header className={cn('flex items-center justify-between', className)} {...props}>
|
||||||
<Link href="/">
|
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
|
||||||
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
|
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center gap-x-6">
|
<div className="hidden items-center gap-x-6 md:flex">
|
||||||
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
|
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
|
||||||
Pricing
|
Pricing
|
||||||
</Link>
|
</Link>
|
||||||
@@ -35,6 +42,15 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
|||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<HamburgerMenu
|
||||||
|
onToggleMenuOpen={() => setIsHamburgerMenuOpen((v) => !v)}
|
||||||
|
isMenuOpen={isHamburgerMenuOpen}
|
||||||
|
/>
|
||||||
|
<MobileNavigation
|
||||||
|
isMenuOpen={isHamburgerMenuOpen}
|
||||||
|
onMenuOpenChange={setIsHamburgerMenuOpen}
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Menu, X } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
export interface HamburgerMenuProps {
|
||||||
|
isMenuOpen: boolean;
|
||||||
|
onToggleMenuOpen?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HamburgerMenu = ({ isMenuOpen, onToggleMenuOpen }: HamburgerMenuProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex md:hidden">
|
||||||
|
<Button variant="outline" className="z-20 w-10 p-0" onClick={onToggleMenuOpen}>
|
||||||
|
{isMenuOpen ? <X /> : <Menu />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
121
apps/marketing/src/components/(marketing)/mobile-navigation.tsx
Normal file
121
apps/marketing/src/components/(marketing)/mobile-navigation.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { motion, useReducedMotion } from 'framer-motion';
|
||||||
|
import { Github, MessagesSquare, Twitter } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
||||||
|
|
||||||
|
export type MobileNavigationProps = {
|
||||||
|
isMenuOpen: boolean;
|
||||||
|
onMenuOpenChange?: (_value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MENU_NAVIGATION_LINKS = [
|
||||||
|
{
|
||||||
|
href: '/blog',
|
||||||
|
text: 'Blog',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/pricing',
|
||||||
|
text: 'Pricing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://status.documenso.com',
|
||||||
|
text: 'Status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'mailto:support@documenso.com',
|
||||||
|
text: 'Support',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/privacy',
|
||||||
|
text: 'Privacy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://app.documenso.com/login',
|
||||||
|
text: 'Sign in',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
||||||
|
const shouldReduceMotion = useReducedMotion();
|
||||||
|
|
||||||
|
const handleMenuItemClick = () => {
|
||||||
|
onMenuOpenChange?.(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
||||||
|
<SheetContent className="w-full max-w-[400px]">
|
||||||
|
<Link href="/" className="z-10" onClick={handleMenuItemClick}>
|
||||||
|
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="mt-12 flex w-full flex-col items-start gap-y-4"
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
transition={{
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{MENU_NAVIGATION_LINKS.map(({ href, text }) => (
|
||||||
|
<motion.div
|
||||||
|
key={href}
|
||||||
|
variants={{
|
||||||
|
initial: {
|
||||||
|
opacity: 0,
|
||||||
|
x: shouldReduceMotion ? 0 : 100,
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
opacity: 1,
|
||||||
|
x: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="text-2xl font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
|
href={href}
|
||||||
|
onClick={() => handleMenuItemClick()}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="mx-auto mt-8 flex w-full flex-wrap items-center gap-x-4 gap-y-4 ">
|
||||||
|
<Link
|
||||||
|
href="https://twitter.com/documenso"
|
||||||
|
target="_blank"
|
||||||
|
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
|
>
|
||||||
|
<Twitter className="h-6 w-6" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://github.com/documenso/documenso"
|
||||||
|
target="_blank"
|
||||||
|
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
|
>
|
||||||
|
<Github className="h-6 w-6" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://documen.so/discord"
|
||||||
|
target="_blank"
|
||||||
|
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
|
>
|
||||||
|
<MessagesSquare className="h-6 w-6" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -21,12 +21,12 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { claimPlan } from '~/api/claim-plan/fetcher';
|
import { claimPlan } from '~/api/claim-plan/fetcher';
|
||||||
|
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
import { SignaturePad } from '../signature-pad';
|
|
||||||
|
|
||||||
const ZWidgetFormSchema = z
|
const ZWidgetFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
HTMLAttributes,
|
|
||||||
MouseEvent,
|
|
||||||
PointerEvent,
|
|
||||||
TouchEvent,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { StrokeOptions, getStroke } from 'perfect-freehand';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
import { getSvgPathFromStroke } from './helper';
|
|
||||||
import { Point } from './point';
|
|
||||||
|
|
||||||
const DPI = 2;
|
|
||||||
|
|
||||||
export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
|
|
||||||
onChange?: (_signatureDataUrl: string | null) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SignaturePad = ({ className, onChange, ...props }: SignaturePadProps) => {
|
|
||||||
const $el = useRef<HTMLCanvasElement>(null);
|
|
||||||
|
|
||||||
const [isPressed, setIsPressed] = useState(false);
|
|
||||||
const [points, setPoints] = useState<Point[]>([]);
|
|
||||||
|
|
||||||
const perfectFreehandOptions = useMemo(() => {
|
|
||||||
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
|
|
||||||
|
|
||||||
return {
|
|
||||||
size,
|
|
||||||
thinning: 0.25,
|
|
||||||
streamline: 0.5,
|
|
||||||
smoothing: 0.5,
|
|
||||||
end: {
|
|
||||||
taper: size * 2,
|
|
||||||
},
|
|
||||||
} satisfies StrokeOptions;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onMouseDown = (event: MouseEvent | PointerEvent | TouchEvent) => {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsPressed(true);
|
|
||||||
|
|
||||||
const point = Point.fromEvent(event, DPI, $el.current);
|
|
||||||
|
|
||||||
const newPoints = [...points, point];
|
|
||||||
|
|
||||||
setPoints(newPoints);
|
|
||||||
|
|
||||||
if ($el.current) {
|
|
||||||
const ctx = $el.current.getContext('2d');
|
|
||||||
|
|
||||||
if (ctx) {
|
|
||||||
ctx.save();
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.imageSmoothingQuality = 'high';
|
|
||||||
|
|
||||||
const pathData = new Path2D(
|
|
||||||
getSvgPathFromStroke(getStroke(newPoints, perfectFreehandOptions)),
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.fill(pathData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseMove = (event: MouseEvent | PointerEvent | TouchEvent) => {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPressed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const point = Point.fromEvent(event, DPI, $el.current);
|
|
||||||
|
|
||||||
if (point.distanceTo(points[points.length - 1]) > 5) {
|
|
||||||
const newPoints = [...points, point];
|
|
||||||
|
|
||||||
setPoints(newPoints);
|
|
||||||
|
|
||||||
if ($el.current) {
|
|
||||||
const ctx = $el.current.getContext('2d');
|
|
||||||
|
|
||||||
if (ctx) {
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.imageSmoothingQuality = 'high';
|
|
||||||
|
|
||||||
const pathData = new Path2D(
|
|
||||||
getSvgPathFromStroke(getStroke(points, perfectFreehandOptions)),
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.fill(pathData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addPoint = true) => {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsPressed(false);
|
|
||||||
|
|
||||||
const point = Point.fromEvent(event, DPI, $el.current);
|
|
||||||
|
|
||||||
const newPoints = [...points];
|
|
||||||
|
|
||||||
if (addPoint) {
|
|
||||||
newPoints.push(point);
|
|
||||||
|
|
||||||
setPoints(newPoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($el.current) {
|
|
||||||
const ctx = $el.current.getContext('2d');
|
|
||||||
|
|
||||||
if (ctx) {
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.imageSmoothingQuality = 'high';
|
|
||||||
|
|
||||||
const pathData = new Path2D(
|
|
||||||
getSvgPathFromStroke(getStroke(newPoints, perfectFreehandOptions)),
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.fill(pathData);
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange?.($el.current.toDataURL());
|
|
||||||
}
|
|
||||||
|
|
||||||
setPoints([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseEnter = (event: MouseEvent | PointerEvent | TouchEvent) => {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('buttons' in event && event.buttons === 1) {
|
|
||||||
onMouseDown(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseLeave = (event: MouseEvent | PointerEvent | TouchEvent) => {
|
|
||||||
if (event.cancelable) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseUp(event, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClearClick = () => {
|
|
||||||
if ($el.current) {
|
|
||||||
const ctx = $el.current.getContext('2d');
|
|
||||||
|
|
||||||
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange?.(null);
|
|
||||||
|
|
||||||
setPoints([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ($el.current) {
|
|
||||||
$el.current.width = $el.current.clientWidth * DPI;
|
|
||||||
$el.current.height = $el.current.clientHeight * DPI;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative block">
|
|
||||||
<canvas
|
|
||||||
ref={$el}
|
|
||||||
className={cn('relative block', className)}
|
|
||||||
style={{ touchAction: 'none' }}
|
|
||||||
onPointerMove={(event) => onMouseMove(event)}
|
|
||||||
onPointerDown={(event) => onMouseDown(event)}
|
|
||||||
onPointerUp={(event) => onMouseUp(event)}
|
|
||||||
onPointerLeave={(event) => onMouseLeave(event)}
|
|
||||||
onPointerEnter={(event) => onMouseEnter(event)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="absolute bottom-2 right-2">
|
|
||||||
<button className="rounded-full p-2 text-xs text-slate-500" onClick={() => onClearClick()}>
|
|
||||||
Clear Signature
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
27
apps/marketing/src/hooks/use-window-size.ts
Normal file
27
apps/marketing/src/hooks/use-window-size.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useWindowSize() {
|
||||||
|
const [size, setSize] = useState({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
setSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onResize();
|
||||||
|
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
@@ -9,16 +9,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": [
|
"~/*": ["./src/*"],
|
||||||
"./src/*"
|
"contentlayer/generated": ["./.contentlayer/generated"]
|
||||||
],
|
|
||||||
"contentlayer/generated": [
|
|
||||||
"./.contentlayer/generated"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"types": [
|
"types": ["@documenso/lib/types/next-auth.d.ts"],
|
||||||
"@documenso/lib/types/next-auth.d.ts"
|
|
||||||
],
|
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"incremental": false
|
"incremental": false
|
||||||
},
|
},
|
||||||
@@ -29,7 +23,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".contentlayer/generated"
|
".contentlayer/generated"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules"]
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"dev": "PORT=3000 next dev",
|
"dev": "PORT=3000 next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
@@ -34,7 +35,6 @@
|
|||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-pdf": "^7.1.1",
|
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
|
|||||||
56611
apps/web/public/pdf.worker.min.js
vendored
Normal file
56611
apps/web/public/pdf.worker.min.js
vendored
Normal 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 { Loader } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useCreateDocument } from '~/api/document/create/fetcher';
|
import { useCreateDocument } from '~/api/document/create/fetcher';
|
||||||
import { DocumentDropzone } from '~/components/(dashboard)/document-dropzone/document-dropzone';
|
|
||||||
|
|
||||||
export type UploadDocumentProps = {
|
export type UploadDocumentProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -2,14 +2,23 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Document, Field, Recipient, User } from '@documenso/prisma/client';
|
import { Document, Field, Recipient, User } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
|
||||||
|
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
||||||
|
import { AddSignersFormPartial } from '@documenso/ui/primitives/document-flow/add-signers';
|
||||||
|
import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
|
||||||
|
import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/add-subject';
|
||||||
|
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
|
||||||
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { LazyPDFViewer } from '~/components/(dashboard)/pdf-viewer/lazy-pdf-viewer';
|
import { addFields } from '~/components/forms/edit-document/add-fields.action';
|
||||||
import { AddFieldsFormPartial } from '~/components/forms/edit-document/add-fields';
|
import { addSigners } from '~/components/forms/edit-document/add-signers.action';
|
||||||
import { AddSignersFormPartial } from '~/components/forms/edit-document/add-signers';
|
import { completeDocument } from '~/components/forms/edit-document/add-subject.action';
|
||||||
import { AddSubjectFormPartial } from '~/components/forms/edit-document/add-subject';
|
|
||||||
|
|
||||||
export type EditDocumentFormProps = {
|
export type EditDocumentFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -26,6 +35,9 @@ export const EditDocumentForm = ({
|
|||||||
fields,
|
fields,
|
||||||
user: _user,
|
user: _user,
|
||||||
}: EditDocumentFormProps) => {
|
}: EditDocumentFormProps) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const [step, setStep] = useState<'signers' | 'fields' | 'subject'>('signers');
|
const [step, setStep] = useState<'signers' | 'fields' | 'subject'>('signers');
|
||||||
|
|
||||||
const documentUrl = `data:application/pdf;base64,${document.document}`;
|
const documentUrl = `data:application/pdf;base64,${document.document}`;
|
||||||
@@ -50,6 +62,76 @@ export const EditDocumentForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
||||||
|
try {
|
||||||
|
// Custom invocation server action
|
||||||
|
await addSigners({
|
||||||
|
documentId: document.id,
|
||||||
|
signers: data.signers,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
|
|
||||||
|
onNextStep();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'An error occurred while adding signers.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddFieldsFormSubmit = async (data: TAddFieldsFormSchema) => {
|
||||||
|
try {
|
||||||
|
// Custom invocation server action
|
||||||
|
await addFields({
|
||||||
|
documentId: document.id,
|
||||||
|
fields: data.fields,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
|
|
||||||
|
onNextStep();
|
||||||
|
} 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();
|
||||||
|
|
||||||
|
onNextStep();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'An error occurred while sending the document.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
|
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
|
||||||
<Card
|
<Card
|
||||||
@@ -69,6 +151,7 @@ export const EditDocumentForm = ({
|
|||||||
document={document}
|
document={document}
|
||||||
onContinue={onNextStep}
|
onContinue={onNextStep}
|
||||||
onGoBack={onPreviousStep}
|
onGoBack={onPreviousStep}
|
||||||
|
onSubmit={onAddSignersFormSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -79,6 +162,7 @@ export const EditDocumentForm = ({
|
|||||||
document={document}
|
document={document}
|
||||||
onContinue={onNextStep}
|
onContinue={onNextStep}
|
||||||
onGoBack={onPreviousStep}
|
onGoBack={onPreviousStep}
|
||||||
|
onSubmit={onAddFieldsFormSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -89,6 +173,7 @@ export const EditDocumentForm = ({
|
|||||||
document={document}
|
document={document}
|
||||||
onContinue={onNextStep}
|
onContinue={onNextStep}
|
||||||
onGoBack={onPreviousStep}
|
onGoBack={onPreviousStep}
|
||||||
|
onSubmit={onAddSubjectFormSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,34 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
import { PDFViewerProps } from '~/components/(dashboard)/pdf-viewer/pdf-viewer';
|
import { PDFViewerProps } from '@documenso/ui/primitives/pdf-viewer';
|
||||||
|
|
||||||
export type LoadablePDFCard = PDFViewerProps & {
|
export type LoadablePDFCard = PDFViewerProps & {
|
||||||
className?: string;
|
className?: string;
|
||||||
pdfClassName?: string;
|
pdfClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PDFViewer = dynamic(async () => import('~/components/(dashboard)/pdf-viewer/pdf-viewer'), {
|
|
||||||
ssr: false,
|
|
||||||
loading: () => (
|
|
||||||
<div className="flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
|
|
||||||
<Loader className="h-12 w-12 animate-spin text-slate-500" />
|
|
||||||
|
|
||||||
<p className="mt-4 text-slate-500">Loading document...</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LoadablePDFCard = ({ className, pdfClassName, ...props }: LoadablePDFCard) => {
|
export const LoadablePDFCard = ({ className, pdfClassName, ...props }: LoadablePDFCard) => {
|
||||||
return (
|
return (
|
||||||
<Card className={className} gradient {...props}>
|
<Card className={className} gradient {...props}>
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-2">
|
||||||
<PDFViewer className={pdfClassName} {...props} />
|
<LazyPDFViewer className={pdfClassName} {...props} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
||||||
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
||||||
import { isDocumentStatus } from '@documenso/lib/types/is-document-status';
|
import { isDocumentStatus } from '@documenso/lib/types/is-document-status';
|
||||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
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 { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
import { DocumentDropzone } from '~/components/(dashboard)/document-dropzone/document-dropzone';
|
|
||||||
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
||||||
import {
|
import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
|
||||||
PeriodSelectorValue,
|
|
||||||
isPeriodSelectorValue,
|
|
||||||
} from '~/components/(dashboard)/period-selector/types';
|
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
|
|
||||||
|
import { UploadDocument } from '../dashboard/upload-document';
|
||||||
import { DocumentsDataTable } from './data-table';
|
import { DocumentsDataTable } from './data-table';
|
||||||
|
|
||||||
export type DocumentsPageProps = {
|
export type DocumentsPageProps = {
|
||||||
@@ -37,10 +31,12 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
});
|
});
|
||||||
|
|
||||||
const status = isDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
|
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 page = Number(searchParams.page) || 1;
|
||||||
const perPage = Number(searchParams.perPage) || 20;
|
const perPage = Number(searchParams.perPage) || 20;
|
||||||
|
|
||||||
|
const shouldDefaultToPending = status === 'ALL' && stats.PENDING > 0;
|
||||||
|
|
||||||
const results = await findDocuments({
|
const results = await findDocuments({
|
||||||
userId: session.id,
|
userId: session.id,
|
||||||
status: status === 'ALL' ? undefined : status,
|
status: status === 'ALL' ? undefined : status,
|
||||||
@@ -52,8 +48,6 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
perPage,
|
perPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isNoResults = status === 'ALL' && period === '' && results.data.length === 0;
|
|
||||||
|
|
||||||
const getTabHref = (value: typeof status) => {
|
const getTabHref = (value: typeof status) => {
|
||||||
const params = new URLSearchParams(searchParams);
|
const params = new URLSearchParams(searchParams);
|
||||||
|
|
||||||
@@ -72,25 +66,13 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<h1 className="text-4xl font-semibold">All Documents</h1>
|
<UploadDocument />
|
||||||
|
|
||||||
|
<h1 className="mt-12 text-4xl font-semibold">Documents</h1>
|
||||||
|
|
||||||
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
|
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
|
||||||
<Tabs defaultValue={status}>
|
<Tabs defaultValue={shouldDefaultToPending ? InternalDocumentStatus.PENDING : status}>
|
||||||
<TabsList>
|
<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>
|
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.PENDING} asChild>
|
||||||
<Link href={getTabHref(InternalDocumentStatus.PENDING)}>
|
<Link href={getTabHref(InternalDocumentStatus.PENDING)}>
|
||||||
<DocumentStatus status={InternalDocumentStatus.PENDING} />
|
<DocumentStatus status={InternalDocumentStatus.PENDING} />
|
||||||
@@ -110,26 +92,30 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</TabsTrigger>
|
</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>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="flex flex-1 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
<div className="flex flex-1 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
||||||
<PeriodSelector />
|
<PeriodSelector />
|
||||||
|
|
||||||
<Button>
|
|
||||||
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
|
||||||
Add Document
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
{/* If we're viewing all documents for all time and there's nuffin we should should an add document component instead */}
|
|
||||||
{isNoResults ? (
|
|
||||||
<DocumentDropzone className="min-h-[60vh] md:min-h-[40vh]" />
|
|
||||||
) : (
|
|
||||||
<DocumentsDataTable results={results} />
|
<DocumentsDataTable results={results} />
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { SignaturePad } from '~/components/signature-pad';
|
|
||||||
|
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { useRequiredSigningContext } from './provider';
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
@@ -9,9 +10,7 @@ import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-re
|
|||||||
import { FieldType } from '@documenso/prisma/client';
|
import { FieldType } from '@documenso/prisma/client';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
import { LazyPDFViewer } from '~/components/(dashboard)/pdf-viewer/lazy-pdf-viewer';
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
|
|
||||||
|
|
||||||
import { DateField } from './date-field';
|
import { DateField } from './date-field';
|
||||||
import { SigningForm } from './form';
|
import { SigningForm } from './form';
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import { trpc } from '@documenso/trpc/react';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
|
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { SignaturePad } from '~/components/signature-pad';
|
|
||||||
|
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { useRequiredSigningContext } from './provider';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { SigningFieldContainer } from './signing-field-container';
|
||||||
|
|
||||||
|
|||||||
@@ -2,30 +2,15 @@
|
|||||||
|
|
||||||
import { HTMLAttributes } from 'react';
|
import { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('ml-8 hidden flex-1 gap-x-6 md:flex', className)} {...props}>
|
<div className={cn('ml-8 hidden flex-1 gap-x-6 md:flex', className)} {...props}>
|
||||||
<Link
|
{/* No Nav tabs while there is only one main page */}
|
||||||
href="/dashboard"
|
{/* <Link
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
|
||||||
{
|
|
||||||
'text-foreground': pathname?.startsWith('/dashboard'),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/documents"
|
href="/documents"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 ',
|
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 ',
|
||||||
@@ -35,14 +20,6 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Documents
|
Documents
|
||||||
</Link>
|
|
||||||
{/* <Link
|
|
||||||
href="/settings/profile"
|
|
||||||
className={cn('font-medium leading-5 text-[#A1A1AA] hover:opacity-80', {
|
|
||||||
'text-primary-foreground': pathname?.startsWith('/settings'),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</Link> */}
|
</Link> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -21,12 +21,12 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { claimPlan } from '~/api/claim-plan/fetcher';
|
import { claimPlan } from '~/api/claim-plan/fetcher';
|
||||||
|
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
import { SignaturePad } from '../signature-pad';
|
|
||||||
|
|
||||||
const ZWidgetFormSchema = z
|
const ZWidgetFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ type FriendlyStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
||||||
DRAFT: {
|
|
||||||
label: 'Draft',
|
|
||||||
icon: File,
|
|
||||||
color: 'text-yellow-500',
|
|
||||||
},
|
|
||||||
PENDING: {
|
PENDING: {
|
||||||
label: 'Pending',
|
label: 'Pending',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
@@ -28,6 +23,11 @@ const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
|||||||
icon: CheckCircle2,
|
icon: CheckCircle2,
|
||||||
color: 'text-green-500',
|
color: 'text-green-500',
|
||||||
},
|
},
|
||||||
|
DRAFT: {
|
||||||
|
label: 'Draft',
|
||||||
|
icon: File,
|
||||||
|
color: 'text-yellow-500',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
||||||
|
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
||||||
import { TAddFieldsFormSchema } from './add-fields.types';
|
|
||||||
|
|
||||||
export type AddFieldsActionInput = TAddFieldsFormSchema & {
|
export type AddFieldsActionInput = TAddFieldsFormSchema & {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||||
|
import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
|
||||||
import { TAddSignersFormSchema } from './add-signers.types';
|
|
||||||
|
|
||||||
export type AddSignersActionInput = TAddSignersFormSchema & {
|
export type AddSignersActionInput = TAddSignersFormSchema & {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
|
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
|
||||||
import { TAddSubjectFormSchema } from './add-subject.types';
|
|
||||||
|
|
||||||
export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import { cn } from '@documenso/ui/lib/utils';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
import { SignaturePad } from '../signature-pad';
|
|
||||||
|
|
||||||
export const ZProfileFormSchema = z.object({
|
export const ZProfileFormSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
await signIn('credentials', {
|
await signIn('credentials', {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
callbackUrl: '/dashboard',
|
callbackUrl: '/documents',
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import { cn } from '@documenso/ui/lib/utils';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { SignaturePad } from '../signature-pad';
|
|
||||||
|
|
||||||
export const ZSignUpFormSchema = z.object({
|
export const ZSignUpFormSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||||
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { Field } from '@documenso/prisma/client';
|
import { Field } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
|
|
||||||
import { getBoundingClientRect } from '~/helpers/get-bounding-client-rect';
|
|
||||||
|
|
||||||
export const useFieldPageCoords = (field: Field) => {
|
export const useFieldPageCoords = (field: Field) => {
|
||||||
const [coords, setCoords] = useState({
|
const [coords, setCoords] = useState({
|
||||||
x: 0,
|
x: 0,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
export default async function middleware(req: NextRequest) {
|
export default async function middleware(req: NextRequest) {
|
||||||
if (req.nextUrl.pathname === '/') {
|
if (req.nextUrl.pathname === '/') {
|
||||||
const redirectUrl = new URL('/dashboard', req.url);
|
const redirectUrl = new URL('/documents', req.url);
|
||||||
|
|
||||||
return NextResponse.redirect(redirectUrl);
|
return NextResponse.redirect(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": [
|
"~/*": ["./src/*"],
|
||||||
"./src/*"
|
"contentlayer/generated": ["./.contentlayer/generated"]
|
||||||
],
|
|
||||||
"contentlayer/generated": [
|
|
||||||
"./.contentlayer/generated"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"types": [
|
"types": ["@documenso/lib/types/next-auth.d.ts"],
|
||||||
"@documenso/lib/types/next-auth.d.ts"
|
|
||||||
],
|
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"incremental": false
|
"incremental": false
|
||||||
},
|
},
|
||||||
@@ -30,7 +24,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".contentlayer/generated"
|
".contentlayer/generated"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules"]
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -23,6 +23,10 @@
|
|||||||
"lint-staged": "^14.0.0",
|
"lint-staged": "^14.0.0",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"turbo": "^1.9.3"
|
"turbo": "^1.9.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=8.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/marketing": {
|
"apps/marketing": {
|
||||||
@@ -87,7 +91,6 @@
|
|||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-pdf": "^7.1.1",
|
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
@@ -12851,9 +12854,9 @@
|
|||||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||||
},
|
},
|
||||||
"node_modules/react-pdf": {
|
"node_modules/react-pdf": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.3.tgz",
|
||||||
"integrity": "sha512-FRUqxGqh/l9TfND7T9ooW5ulZZt1tPYVwSEonFv3ImQFp3OkPM09BljchwEGHszP7P6h/Tt8js6v6d/T7GTC3A==",
|
"integrity": "sha512-d7WAxcsjOogJfJ+I+zX/mdip3VjR1yq/yDa4hax4XbQVjbbbup6rqs4c8MGx0MLSnzob17TKp1t4CsNbDZ6GeQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"make-cancellable-promise": "^1.3.1",
|
"make-cancellable-promise": "^1.3.1",
|
||||||
@@ -16197,7 +16200,10 @@
|
|||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.214.0",
|
"lucide-react": "^0.214.0",
|
||||||
|
"next": "13.4.12",
|
||||||
|
"pdfjs-dist": "3.6.172",
|
||||||
"react-day-picker": "^8.7.1",
|
"react-day-picker": "^8.7.1",
|
||||||
|
"react-pdf": "^7.3.3",
|
||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
"tailwindcss-animate": "^1.0.5"
|
"tailwindcss-animate": "^1.0.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"commitlint": "commitlint --edit"
|
"commitlint": "commitlint --edit"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"npm": ">=8.6.0",
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.7.1",
|
"@commitlint/cli": "^17.7.1",
|
||||||
"@commitlint/config-conventional": "^17.7.0",
|
"@commitlint/config-conventional": "^17.7.0",
|
||||||
|
|||||||
@@ -54,7 +54,10 @@
|
|||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.214.0",
|
"lucide-react": "^0.214.0",
|
||||||
|
"next": "13.4.12",
|
||||||
|
"pdfjs-dist": "3.6.172",
|
||||||
"react-day-picker": "^8.7.1",
|
"react-day-picker": "^8.7.1",
|
||||||
|
"react-pdf": "^7.3.3",
|
||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
"tailwindcss-animate": "^1.0.5"
|
"tailwindcss-animate": "^1.0.5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Caveat } from 'next/font/google';
|
import { Caveat } from 'next/font/google';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Check, ChevronsUpDown, Info } from 'lucide-react';
|
import { Check, ChevronsUpDown, Info } from 'lucide-react';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||||
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { Document, Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client';
|
import { Document, Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@@ -22,20 +23,15 @@ import {
|
|||||||
} from '@documenso/ui/primitives/command';
|
} from '@documenso/ui/primitives/command';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
|
|
||||||
import { getBoundingClientRect } from '~/helpers/get-bounding-client-rect';
|
|
||||||
|
|
||||||
import { addFields } from './add-fields.action';
|
|
||||||
import { TAddFieldsFormSchema } from './add-fields.types';
|
import { TAddFieldsFormSchema } from './add-fields.types';
|
||||||
import {
|
import {
|
||||||
EditDocumentFormContainer,
|
DocumentFlowFormContainer,
|
||||||
EditDocumentFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
EditDocumentFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
EditDocumentFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
EditDocumentFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './container';
|
} from './document-flow-root';
|
||||||
import { FieldItem } from './field-item';
|
import { FieldItem } from './field-item';
|
||||||
import { FRIENDLY_FIELD_TYPE } from './types';
|
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||||
|
|
||||||
@@ -58,18 +54,15 @@ export type AddFieldsFormProps = {
|
|||||||
document: Document;
|
document: Document;
|
||||||
onContinue?: () => void;
|
onContinue?: () => void;
|
||||||
onGoBack?: () => void;
|
onGoBack?: () => void;
|
||||||
|
onSubmit: (_data: TAddFieldsFormSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddFieldsFormPartial = ({
|
export const AddFieldsFormPartial = ({
|
||||||
recipients,
|
recipients,
|
||||||
fields,
|
fields,
|
||||||
document,
|
|
||||||
onContinue,
|
|
||||||
onGoBack,
|
onGoBack,
|
||||||
|
onSubmit,
|
||||||
}: AddFieldsFormProps) => {
|
}: AddFieldsFormProps) => {
|
||||||
const { toast } = useToast();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -303,28 +296,6 @@ export const AddFieldsFormPartial = ({
|
|||||||
[localFields, update],
|
[localFields, update],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFormSubmit = handleSubmit(async (data: TAddFieldsFormSchema) => {
|
|
||||||
try {
|
|
||||||
// Custom invocation server action
|
|
||||||
await addFields({
|
|
||||||
documentId: document.id,
|
|
||||||
fields: data.fields,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
onContinue?.();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Error',
|
|
||||||
description: 'An error occurred while adding signers.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedField) {
|
if (selectedField) {
|
||||||
window.addEventListener('mousemove', onMouseMove);
|
window.addEventListener('mousemove', onMouseMove);
|
||||||
@@ -357,8 +328,8 @@ export const AddFieldsFormPartial = ({
|
|||||||
}, [recipients]);
|
}, [recipients]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditDocumentFormContainer>
|
<DocumentFlowFormContainer>
|
||||||
<EditDocumentFormContainerContent
|
<DocumentFlowFormContainerContent
|
||||||
title="Add Fields"
|
title="Add Fields"
|
||||||
description="Add all relevant fields for each recipient."
|
description="Add all relevant fields for each recipient."
|
||||||
>
|
>
|
||||||
@@ -560,18 +531,18 @@ export const AddFieldsFormPartial = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</EditDocumentFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|
||||||
<EditDocumentFormContainerFooter>
|
<DocumentFlowFormContainerFooter>
|
||||||
<EditDocumentFormContainerStep title="Add Fields" step={2} maxStep={3} />
|
<DocumentFlowFormContainerStep title="Add Fields" step={2} maxStep={3} />
|
||||||
|
|
||||||
<EditDocumentFormContainerActions
|
<DocumentFlowFormContainerActions
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onGoNextClick={() => onFormSubmit()}
|
onGoNextClick={() => handleSubmit(onSubmit)()}
|
||||||
onGoBackClick={onGoBack}
|
onGoBackClick={onGoBack}
|
||||||
/>
|
/>
|
||||||
</EditDocumentFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
</EditDocumentFormContainer>
|
</DocumentFlowFormContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import React, { useId } from 'react';
|
import React, { useId } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Plus, Trash } from 'lucide-react';
|
import { Plus, Trash } from 'lucide-react';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
@@ -11,21 +9,19 @@ import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
|||||||
|
|
||||||
import { Document, Field, Recipient, SendStatus } from '@documenso/prisma/client';
|
import { Document, Field, Recipient, SendStatus } from '@documenso/prisma/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { FormErrorMessage } from '~/components/form/form-error-message';
|
|
||||||
|
|
||||||
import { addSigners } from './add-signers.action';
|
|
||||||
import { TAddSignersFormSchema } from './add-signers.types';
|
import { TAddSignersFormSchema } from './add-signers.types';
|
||||||
import {
|
import {
|
||||||
EditDocumentFormContainer,
|
DocumentFlowFormContainer,
|
||||||
EditDocumentFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
EditDocumentFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
EditDocumentFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
EditDocumentFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './container';
|
} from './document-flow-root';
|
||||||
|
|
||||||
export type AddSignersFormProps = {
|
export type AddSignersFormProps = {
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
@@ -33,17 +29,16 @@ export type AddSignersFormProps = {
|
|||||||
document: Document;
|
document: Document;
|
||||||
onContinue?: () => void;
|
onContinue?: () => void;
|
||||||
onGoBack?: () => void;
|
onGoBack?: () => void;
|
||||||
|
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddSignersFormPartial = ({
|
export const AddSignersFormPartial = ({
|
||||||
recipients,
|
recipients,
|
||||||
fields: _fields,
|
fields: _fields,
|
||||||
document: document,
|
|
||||||
onContinue,
|
|
||||||
onGoBack,
|
onGoBack,
|
||||||
|
onSubmit,
|
||||||
}: AddSignersFormProps) => {
|
}: AddSignersFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const initialId = useId();
|
const initialId = useId();
|
||||||
|
|
||||||
@@ -120,31 +115,9 @@ export const AddSignersFormPartial = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFormSubmit = handleSubmit(async (data: TAddSignersFormSchema) => {
|
|
||||||
try {
|
|
||||||
// Custom invocation server action
|
|
||||||
await addSigners({
|
|
||||||
documentId: document.id,
|
|
||||||
signers: data.signers,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
onContinue?.();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Error',
|
|
||||||
description: 'An error occurred while adding signers.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditDocumentFormContainer onSubmit={onFormSubmit}>
|
<DocumentFlowFormContainer onSubmit={handleSubmit(onSubmit)}>
|
||||||
<EditDocumentFormContainerContent
|
<DocumentFlowFormContainerContent
|
||||||
title="Add Signers"
|
title="Add Signers"
|
||||||
description="Add the people who will sign the document."
|
description="Add the people who will sign the document."
|
||||||
>
|
>
|
||||||
@@ -229,18 +202,18 @@ export const AddSignersFormPartial = ({
|
|||||||
Add Signer
|
Add Signer
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</EditDocumentFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|
||||||
<EditDocumentFormContainerFooter>
|
<DocumentFlowFormContainerFooter>
|
||||||
<EditDocumentFormContainerStep title="Add Signers" step={1} maxStep={3} />
|
<DocumentFlowFormContainerStep title="Add Signers" step={1} maxStep={3} />
|
||||||
|
|
||||||
<EditDocumentFormContainerActions
|
<DocumentFlowFormContainerActions
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onGoNextClick={() => onFormSubmit()}
|
onGoNextClick={() => handleSubmit(onSubmit)()}
|
||||||
onGoBackClick={onGoBack}
|
onGoBackClick={onGoBack}
|
||||||
/>
|
/>
|
||||||
</EditDocumentFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
</EditDocumentFormContainer>
|
</DocumentFlowFormContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,26 +1,22 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { Document, DocumentStatus, Field, Recipient } from '@documenso/prisma/client';
|
import { Document, DocumentStatus, Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { Textarea } from '@documenso/ui/primitives/textarea';
|
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { FormErrorMessage } from '~/components/form/form-error-message';
|
import { FormErrorMessage } from '~/components/form/form-error-message';
|
||||||
|
|
||||||
import { completeDocument } from './add-subject.action';
|
|
||||||
import { TAddSubjectFormSchema } from './add-subject.types';
|
import { TAddSubjectFormSchema } from './add-subject.types';
|
||||||
import {
|
import {
|
||||||
EditDocumentFormContainer,
|
DocumentFlowFormContainer,
|
||||||
EditDocumentFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
EditDocumentFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
EditDocumentFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
EditDocumentFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './container';
|
} from './document-flow-root';
|
||||||
|
|
||||||
export type AddSubjectFormProps = {
|
export type AddSubjectFormProps = {
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
@@ -28,18 +24,16 @@ export type AddSubjectFormProps = {
|
|||||||
document: Document;
|
document: Document;
|
||||||
onContinue?: () => void;
|
onContinue?: () => void;
|
||||||
onGoBack?: () => void;
|
onGoBack?: () => void;
|
||||||
|
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddSubjectFormPartial = ({
|
export const AddSubjectFormPartial = ({
|
||||||
recipients: _recipients,
|
recipients: _recipients,
|
||||||
fields: _fields,
|
fields: _fields,
|
||||||
document,
|
document,
|
||||||
onContinue,
|
|
||||||
onGoBack,
|
onGoBack,
|
||||||
|
onSubmit,
|
||||||
}: AddSubjectFormProps) => {
|
}: AddSubjectFormProps) => {
|
||||||
const { toast } = useToast();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -53,35 +47,9 @@ export const AddSubjectFormPartial = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = handleSubmit(async (data: TAddSubjectFormSchema) => {
|
|
||||||
const { subject, message } = data.email;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await completeDocument({
|
|
||||||
documentId: document.id,
|
|
||||||
email: {
|
|
||||||
subject,
|
|
||||||
message,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
onContinue?.();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Error',
|
|
||||||
description: 'An error occurred while sending the document.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditDocumentFormContainer>
|
<DocumentFlowFormContainer>
|
||||||
<EditDocumentFormContainerContent
|
<DocumentFlowFormContainerContent
|
||||||
title="Add Subject"
|
title="Add Subject"
|
||||||
description="Add the subject and message you wish to send to signers."
|
description="Add the subject and message you wish to send to signers."
|
||||||
>
|
>
|
||||||
@@ -151,19 +119,19 @@ export const AddSubjectFormPartial = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</EditDocumentFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|
||||||
<EditDocumentFormContainerFooter>
|
<DocumentFlowFormContainerFooter>
|
||||||
<EditDocumentFormContainerStep title="Add Subject" step={3} maxStep={3} />
|
<DocumentFlowFormContainerStep title="Add Subject" step={3} maxStep={3} />
|
||||||
|
|
||||||
<EditDocumentFormContainerActions
|
<DocumentFlowFormContainerActions
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
|
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
|
||||||
onGoNextClick={() => onFormSubmit()}
|
onGoNextClick={() => handleSubmit(onSubmit)()}
|
||||||
onGoBackClick={onGoBack}
|
onGoBackClick={onGoBack}
|
||||||
/>
|
/>
|
||||||
</EditDocumentFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
</EditDocumentFormContainer>
|
</DocumentFlowFormContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -7,16 +7,16 @@ import { Loader } from 'lucide-react';
|
|||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
export type EditDocumentFormContainerProps = HTMLAttributes<HTMLFormElement> & {
|
export type DocumentFlowFormContainerProps = HTMLAttributes<HTMLFormElement> & {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditDocumentFormContainer = ({
|
export const DocumentFlowFormContainer = ({
|
||||||
children,
|
children,
|
||||||
id = 'edit-document-form',
|
id = 'edit-document-form',
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: EditDocumentFormContainerProps) => {
|
}: DocumentFlowFormContainerProps) => {
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
id={id}
|
id={id}
|
||||||
@@ -31,19 +31,19 @@ export const EditDocumentFormContainer = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditDocumentFormContainerContentProps = HTMLAttributes<HTMLDivElement> & {
|
export type DocumentFlowFormContainerContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditDocumentFormContainerContent = ({
|
export const DocumentFlowFormContainerContent = ({
|
||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: EditDocumentFormContainerContentProps) => {
|
}: DocumentFlowFormContainerContentProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-1 flex-col', className)} {...props}>
|
<div className={cn('flex flex-1 flex-col', className)} {...props}>
|
||||||
<h3 className="text-foreground text-2xl font-semibold">{title}</h3>
|
<h3 className="text-foreground text-2xl font-semibold">{title}</h3>
|
||||||
@@ -57,15 +57,15 @@ export const EditDocumentFormContainerContent = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditDocumentFormContainerFooterProps = HTMLAttributes<HTMLDivElement> & {
|
export type DocumentFlowFormContainerFooterProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditDocumentFormContainerFooter = ({
|
export const DocumentFlowFormContainerFooter = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: EditDocumentFormContainerFooterProps) => {
|
}: DocumentFlowFormContainerFooterProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn('mt-4 flex-shrink-0', className)} {...props}>
|
<div className={cn('mt-4 flex-shrink-0', className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
@@ -73,17 +73,17 @@ export const EditDocumentFormContainerFooter = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditDocumentFormContainerStepProps = {
|
export type DocumentFlowFormContainerStepProps = {
|
||||||
title: string;
|
title: string;
|
||||||
step: number;
|
step: number;
|
||||||
maxStep: number;
|
maxStep: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditDocumentFormContainerStep = ({
|
export const DocumentFlowFormContainerStep = ({
|
||||||
title,
|
title,
|
||||||
step,
|
step,
|
||||||
maxStep,
|
maxStep,
|
||||||
}: EditDocumentFormContainerStepProps) => {
|
}: DocumentFlowFormContainerStepProps) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
@@ -105,7 +105,7 @@ export const EditDocumentFormContainerStep = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditDocumentFormContainerActionsProps = {
|
export type DocumentFlowFormContainerActionsProps = {
|
||||||
canGoBack?: boolean;
|
canGoBack?: boolean;
|
||||||
canGoNext?: boolean;
|
canGoNext?: boolean;
|
||||||
goNextLabel?: string;
|
goNextLabel?: string;
|
||||||
@@ -116,7 +116,7 @@ export type EditDocumentFormContainerActionsProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditDocumentFormContainerActions = ({
|
export const DocumentFlowFormContainerActions = ({
|
||||||
canGoBack = true,
|
canGoBack = true,
|
||||||
canGoNext = true,
|
canGoNext = true,
|
||||||
goNextLabel = 'Continue',
|
goNextLabel = 'Continue',
|
||||||
@@ -125,7 +125,7 @@ export const EditDocumentFormContainerActions = ({
|
|||||||
onGoNextClick,
|
onGoNextClick,
|
||||||
loading,
|
loading,
|
||||||
disabled,
|
disabled,
|
||||||
}: EditDocumentFormContainerActionsProps) => {
|
}: DocumentFlowFormContainerActionsProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex gap-x-4">
|
<div className="mt-4 flex gap-x-4">
|
||||||
<Button
|
<Button
|
||||||
@@ -6,14 +6,13 @@ import { Trash } from 'lucide-react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Rnd } from 'react-rnd';
|
import { Rnd } from 'react-rnd';
|
||||||
|
|
||||||
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
|
import { FRIENDLY_FIELD_TYPE, TDocumentFlowFormSchema } from './types';
|
||||||
|
|
||||||
import { FRIENDLY_FIELD_TYPE, TEditDocumentFormSchema } from './types';
|
type Field = TDocumentFlowFormSchema['fields'][0];
|
||||||
|
|
||||||
type Field = TEditDocumentFormSchema['fields'][0];
|
|
||||||
|
|
||||||
export type FieldItemProps = {
|
export type FieldItemProps = {
|
||||||
field: Field;
|
field: Field;
|
||||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
import { FieldType } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const ZEditDocumentFormSchema = z.object({
|
export const ZDocumentFlowFormSchema = z.object({
|
||||||
signers: z
|
signers: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -37,7 +37,7 @@ export const ZEditDocumentFormSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TEditDocumentFormSchema = z.infer<typeof ZEditDocumentFormSchema>;
|
export type TDocumentFlowFormSchema = z.infer<typeof ZDocumentFlowFormSchema>;
|
||||||
|
|
||||||
export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
|
export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
|
||||||
[FieldType.SIGNATURE]: 'Signature',
|
[FieldType.SIGNATURE]: 'Signature',
|
||||||
34
packages/ui/primitives/form/form-error-message.tsx
Normal file
34
packages/ui/primitives/form/form-error-message.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
export type FormErrorMessageProps = {
|
||||||
|
className?: string;
|
||||||
|
error: { message?: string } | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) => {
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{error && (
|
||||||
|
<motion.p
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
y: -10,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
y: 10,
|
||||||
|
}}
|
||||||
|
className={cn('text-xs text-red-500', className)}
|
||||||
|
>
|
||||||
|
{error.message}
|
||||||
|
</motion.p>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
16
packages/ui/primitives/lazy-pdf-viewer.tsx
Normal file
16
packages/ui/primitives/lazy-pdf-viewer.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
import { Loader } from 'lucide-react';
|
||||||
|
|
||||||
|
export const LazyPDFViewer = dynamic(async () => import('./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>
|
||||||
|
),
|
||||||
|
});
|
||||||
@@ -3,21 +3,19 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
||||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
type LoadedPDFDocument = pdfjs.PDFDocumentProxy;
|
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This imports the worker from the `pdfjs-dist` package.
|
* This imports the worker from the `pdfjs-dist` package.
|
||||||
*/
|
*/
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjs.GlobalWorkerOptions.workerSrc = `/pdf.worker.min.js`;
|
||||||
'pdfjs-dist/build/pdf.worker.min.js',
|
|
||||||
import.meta.url,
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
export type OnPDFViewerPageClick = (_event: {
|
export type OnPDFViewerPageClick = (_event: {
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
@@ -194,7 +194,6 @@ export const SignaturePad = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log({ defaultValue });
|
|
||||||
if ($el.current && typeof defaultValue === 'string') {
|
if ($el.current && typeof defaultValue === 'string') {
|
||||||
const ctx = $el.current.getContext('2d');
|
const ctx = $el.current.getContext('2d');
|
||||||
|
|
||||||
11
scripts/copy-pdfjs.cjs
Normal file
11
scripts/copy-pdfjs.cjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
|
||||||
|
|
||||||
|
const pdfWorkerPath = path.join(pdfjsDistPath, 'build', 'pdf.worker.min.js');
|
||||||
|
|
||||||
|
fs.copyFileSync(pdfWorkerPath, './public/pdf.worker.min.js');
|
||||||
Reference in New Issue
Block a user