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:
|
||||
branches: [ "feat/refresh" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
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)
|
||||
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
|
||||
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
|
||||
- [Zod for schema declaration and validation](https://zod.dev/)
|
||||
- [Lucide React for icons in React app](https://lucide.dev/)
|
||||
- [Framer Motion for motion library](https://www.framer.com/motion/)
|
||||
- [Radix UI for component library](https://www.radix-ui.com/)
|
||||
- Check out `/package.json` and `/apps/web/package.json` for more
|
||||
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
|
||||
|
||||
|
||||
56
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",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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 Link from 'next/link';
|
||||
|
||||
import { Github, Slack, Twitter } from 'lucide-react';
|
||||
import { Github, MessagesSquare, Twitter } from 'lucide-react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
@@ -36,11 +36,11 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://documenso.slack.com"
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
className="hover:text-[#6D6D6D]"
|
||||
>
|
||||
<Slack className="h-6 w-6" />
|
||||
<MessagesSquare className="h-6 w-6" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,6 +61,14 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
Open
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://shop.documenso.com"
|
||||
target="_blank"
|
||||
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||
>
|
||||
Shop
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://status.documenso.com"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes, useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import { HamburgerMenu } from './mobile-hamburger';
|
||||
import { MobileNavigation } from './mobile-navigation';
|
||||
|
||||
export type HeaderProps = HTMLAttributes<HTMLElement>;
|
||||
|
||||
export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className={cn('flex items-center justify-between', className)} {...props}>
|
||||
<Link href="/">
|
||||
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
|
||||
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
|
||||
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} />
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-x-6">
|
||||
<div className="hidden items-center gap-x-6 md:flex">
|
||||
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
|
||||
Pricing
|
||||
</Link>
|
||||
@@ -35,6 +42,15 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<HamburgerMenu
|
||||
onToggleMenuOpen={() => setIsHamburgerMenuOpen((v) => !v)}
|
||||
isMenuOpen={isHamburgerMenuOpen}
|
||||
/>
|
||||
<MobileNavigation
|
||||
isMenuOpen={isHamburgerMenuOpen}
|
||||
onMenuOpenChange={setIsHamburgerMenuOpen}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { Menu, X } from 'lucide-react';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export interface HamburgerMenuProps {
|
||||
isMenuOpen: boolean;
|
||||
onToggleMenuOpen?: () => void;
|
||||
}
|
||||
|
||||
export const HamburgerMenu = ({ isMenuOpen, onToggleMenuOpen }: HamburgerMenuProps) => {
|
||||
return (
|
||||
<div className="flex md:hidden">
|
||||
<Button variant="outline" className="z-20 w-10 p-0" onClick={onToggleMenuOpen}>
|
||||
{isMenuOpen ? <X /> : <Menu />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { claimPlan } from '~/api/claim-plan/fetcher';
|
||||
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { SignaturePad } from '../signature-pad';
|
||||
|
||||
const ZWidgetFormSchema = z
|
||||
.object({
|
||||
|
||||
@@ -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": {
|
||||
"~/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"contentlayer/generated": [
|
||||
"./.contentlayer/generated"
|
||||
]
|
||||
"~/*": ["./src/*"],
|
||||
"contentlayer/generated": ["./.contentlayer/generated"]
|
||||
},
|
||||
"types": [
|
||||
"@documenso/lib/types/next-auth.d.ts"
|
||||
],
|
||||
"types": ["@documenso/lib/types/next-auth.d.ts"],
|
||||
"strictNullChecks": true,
|
||||
"incremental": false
|
||||
},
|
||||
@@ -29,7 +23,5 @@
|
||||
".next/types/**/*.ts",
|
||||
".contentlayer/generated"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"dev": "PORT=3000 next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/lib": "*",
|
||||
@@ -34,7 +35,6 @@
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-pdf": "^7.1.1",
|
||||
"react-rnd": "^10.4.1",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"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 { cn } from '@documenso/ui/lib/utils';
|
||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useCreateDocument } from '~/api/document/create/fetcher';
|
||||
import { DocumentDropzone } from '~/components/(dashboard)/document-dropzone/document-dropzone';
|
||||
|
||||
export type UploadDocumentProps = {
|
||||
className?: string;
|
||||
|
||||
@@ -2,14 +2,23 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Document, Field, Recipient, User } from '@documenso/prisma/client';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
|
||||
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
||||
import { AddSignersFormPartial } from '@documenso/ui/primitives/document-flow/add-signers';
|
||||
import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
|
||||
import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/add-subject';
|
||||
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { LazyPDFViewer } from '~/components/(dashboard)/pdf-viewer/lazy-pdf-viewer';
|
||||
import { AddFieldsFormPartial } from '~/components/forms/edit-document/add-fields';
|
||||
import { AddSignersFormPartial } from '~/components/forms/edit-document/add-signers';
|
||||
import { AddSubjectFormPartial } from '~/components/forms/edit-document/add-subject';
|
||||
import { addFields } from '~/components/forms/edit-document/add-fields.action';
|
||||
import { addSigners } from '~/components/forms/edit-document/add-signers.action';
|
||||
import { completeDocument } from '~/components/forms/edit-document/add-subject.action';
|
||||
|
||||
export type EditDocumentFormProps = {
|
||||
className?: string;
|
||||
@@ -26,6 +35,9 @@ export const EditDocumentForm = ({
|
||||
fields,
|
||||
user: _user,
|
||||
}: EditDocumentFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const [step, setStep] = useState<'signers' | 'fields' | 'subject'>('signers');
|
||||
|
||||
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 (
|
||||
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
|
||||
<Card
|
||||
@@ -69,6 +151,7 @@ export const EditDocumentForm = ({
|
||||
document={document}
|
||||
onContinue={onNextStep}
|
||||
onGoBack={onPreviousStep}
|
||||
onSubmit={onAddSignersFormSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -79,6 +162,7 @@ export const EditDocumentForm = ({
|
||||
document={document}
|
||||
onContinue={onNextStep}
|
||||
onGoBack={onPreviousStep}
|
||||
onSubmit={onAddFieldsFormSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -89,6 +173,7 @@ export const EditDocumentForm = ({
|
||||
document={document}
|
||||
onContinue={onNextStep}
|
||||
onGoBack={onPreviousStep}
|
||||
onSubmit={onAddSubjectFormSubmit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { PDFViewerProps } from '~/components/(dashboard)/pdf-viewer/pdf-viewer';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { PDFViewerProps } from '@documenso/ui/primitives/pdf-viewer';
|
||||
|
||||
export type LoadablePDFCard = PDFViewerProps & {
|
||||
className?: string;
|
||||
pdfClassName?: string;
|
||||
};
|
||||
|
||||
const PDFViewer = dynamic(async () => import('~/components/(dashboard)/pdf-viewer/pdf-viewer'), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
|
||||
<Loader className="h-12 w-12 animate-spin text-slate-500" />
|
||||
|
||||
<p className="mt-4 text-slate-500">Loading document...</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
export const LoadablePDFCard = ({ className, pdfClassName, ...props }: LoadablePDFCard) => {
|
||||
return (
|
||||
<Card className={className} gradient {...props}>
|
||||
<CardContent className="p-2">
|
||||
<PDFViewer className={pdfClassName} {...props} />
|
||||
<LazyPDFViewer className={pdfClassName} {...props} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
||||
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
||||
import { isDocumentStatus } from '@documenso/lib/types/is-document-status';
|
||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
import { DocumentDropzone } from '~/components/(dashboard)/document-dropzone/document-dropzone';
|
||||
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
||||
import {
|
||||
PeriodSelectorValue,
|
||||
isPeriodSelectorValue,
|
||||
} from '~/components/(dashboard)/period-selector/types';
|
||||
import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
|
||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||
|
||||
import { UploadDocument } from '../dashboard/upload-document';
|
||||
import { DocumentsDataTable } from './data-table';
|
||||
|
||||
export type DocumentsPageProps = {
|
||||
@@ -37,10 +31,12 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
||||
});
|
||||
|
||||
const status = isDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
|
||||
const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
|
||||
// const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
|
||||
const page = Number(searchParams.page) || 1;
|
||||
const perPage = Number(searchParams.perPage) || 20;
|
||||
|
||||
const shouldDefaultToPending = status === 'ALL' && stats.PENDING > 0;
|
||||
|
||||
const results = await findDocuments({
|
||||
userId: session.id,
|
||||
status: status === 'ALL' ? undefined : status,
|
||||
@@ -52,8 +48,6 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
||||
perPage,
|
||||
});
|
||||
|
||||
const isNoResults = status === 'ALL' && period === '' && results.data.length === 0;
|
||||
|
||||
const getTabHref = (value: typeof status) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
@@ -72,25 +66,13 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
<h1 className="text-4xl font-semibold">All Documents</h1>
|
||||
<UploadDocument />
|
||||
|
||||
<h1 className="mt-12 text-4xl font-semibold">Documents</h1>
|
||||
|
||||
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
|
||||
<Tabs defaultValue={status}>
|
||||
<Tabs defaultValue={shouldDefaultToPending ? InternalDocumentStatus.PENDING : status}>
|
||||
<TabsList>
|
||||
<TabsTrigger className="min-w-[60px]" value="ALL" asChild>
|
||||
<Link href={getTabHref('ALL')}>All</Link>
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.DRAFT} asChild>
|
||||
<Link href={getTabHref(InternalDocumentStatus.DRAFT)}>
|
||||
<DocumentStatus status={InternalDocumentStatus.DRAFT} />
|
||||
|
||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
||||
{Math.min(stats.DRAFT, 99)}
|
||||
</span>
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.PENDING} asChild>
|
||||
<Link href={getTabHref(InternalDocumentStatus.PENDING)}>
|
||||
<DocumentStatus status={InternalDocumentStatus.PENDING} />
|
||||
@@ -110,26 +92,30 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
||||
</span>
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.DRAFT} asChild>
|
||||
<Link href={getTabHref(InternalDocumentStatus.DRAFT)}>
|
||||
<DocumentStatus status={InternalDocumentStatus.DRAFT} />
|
||||
|
||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
||||
{Math.min(stats.DRAFT, 99)}
|
||||
</span>
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger className="min-w-[60px]" value="ALL" asChild>
|
||||
<Link href={getTabHref('ALL')}>All</Link>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex flex-1 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
||||
<PeriodSelector />
|
||||
|
||||
<Button>
|
||||
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
||||
Add Document
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
{/* If we're viewing all documents for all time and there's nuffin we should should an add document component instead */}
|
||||
{isNoResults ? (
|
||||
<DocumentDropzone className="min-h-[60vh] md:min-h-[40vh]" />
|
||||
) : (
|
||||
<DocumentsDataTable results={results} />
|
||||
)}
|
||||
<DocumentsDataTable results={results} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,8 +12,7 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
|
||||
import { SignaturePad } from '~/components/signature-pad';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
|
||||
import { useRequiredSigningContext } from './provider';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
@@ -9,9 +10,7 @@ import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-re
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
|
||||
import { LazyPDFViewer } from '~/components/(dashboard)/pdf-viewer/lazy-pdf-viewer';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
|
||||
import { DateField } from './date-field';
|
||||
import { SigningForm } from './form';
|
||||
|
||||
@@ -12,10 +12,9 @@ import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { SignaturePad } from '~/components/signature-pad';
|
||||
|
||||
import { useRequiredSigningContext } from './provider';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
|
||||
@@ -2,30 +2,15 @@
|
||||
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className={cn('ml-8 hidden flex-1 gap-x-6 md:flex', className)} {...props}>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className={cn(
|
||||
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
||||
{
|
||||
'text-foreground': pathname?.startsWith('/dashboard'),
|
||||
},
|
||||
)}
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
{/* No Nav tabs while there is only one main page */}
|
||||
{/* <Link
|
||||
href="/documents"
|
||||
className={cn(
|
||||
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 ',
|
||||
@@ -35,14 +20,6 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
)}
|
||||
>
|
||||
Documents
|
||||
</Link>
|
||||
{/* <Link
|
||||
href="/settings/profile"
|
||||
className={cn('font-medium leading-5 text-[#A1A1AA] hover:opacity-80', {
|
||||
'text-primary-foreground': pathname?.startsWith('/settings'),
|
||||
})}
|
||||
>
|
||||
Settings
|
||||
</Link> */}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { claimPlan } from '~/api/claim-plan/fetcher';
|
||||
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { SignaturePad } from '../signature-pad';
|
||||
|
||||
const ZWidgetFormSchema = z
|
||||
.object({
|
||||
|
||||
@@ -13,11 +13,6 @@ type FriendlyStatus = {
|
||||
};
|
||||
|
||||
const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
||||
DRAFT: {
|
||||
label: 'Draft',
|
||||
icon: File,
|
||||
color: 'text-yellow-500',
|
||||
},
|
||||
PENDING: {
|
||||
label: 'Pending',
|
||||
icon: Clock,
|
||||
@@ -28,6 +23,11 @@ const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
||||
icon: CheckCircle2,
|
||||
color: 'text-green-500',
|
||||
},
|
||||
DRAFT: {
|
||||
label: 'Draft',
|
||||
icon: File,
|
||||
color: 'text-yellow-500',
|
||||
},
|
||||
};
|
||||
|
||||
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
||||
|
||||
import { TAddFieldsFormSchema } from './add-fields.types';
|
||||
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
||||
|
||||
export type AddFieldsActionInput = TAddFieldsFormSchema & {
|
||||
documentId: number;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||
|
||||
import { TAddSignersFormSchema } from './add-signers.types';
|
||||
import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
|
||||
|
||||
export type AddSignersActionInput = TAddSignersFormSchema & {
|
||||
documentId: number;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
|
||||
import { TAddSubjectFormSchema } from './add-subject.types';
|
||||
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
|
||||
|
||||
export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
||||
documentId: number;
|
||||
|
||||
@@ -14,10 +14,10 @@ import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { SignaturePad } from '../signature-pad';
|
||||
|
||||
export const ZProfileFormSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
|
||||
@@ -44,7 +44,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
||||
await signIn('credentials', {
|
||||
email,
|
||||
password,
|
||||
callbackUrl: '/dashboard',
|
||||
callbackUrl: '/documents',
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
@@ -12,10 +12,9 @@ import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { SignaturePad } from '../signature-pad';
|
||||
|
||||
export const ZSignUpFormSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email().min(1),
|
||||
|
||||
@@ -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 { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { Field } from '@documenso/prisma/client';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
|
||||
import { getBoundingClientRect } from '~/helpers/get-bounding-client-rect';
|
||||
|
||||
export const useFieldPageCoords = (field: Field) => {
|
||||
const [coords, setCoords] = useState({
|
||||
x: 0,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export default async function middleware(req: NextRequest) {
|
||||
if (req.nextUrl.pathname === '/') {
|
||||
const redirectUrl = new URL('/dashboard', req.url);
|
||||
const redirectUrl = new URL('/documents', req.url);
|
||||
|
||||
return NextResponse.redirect(redirectUrl);
|
||||
}
|
||||
|
||||
@@ -9,16 +9,10 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"~/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"contentlayer/generated": [
|
||||
"./.contentlayer/generated"
|
||||
]
|
||||
"~/*": ["./src/*"],
|
||||
"contentlayer/generated": ["./.contentlayer/generated"]
|
||||
},
|
||||
"types": [
|
||||
"@documenso/lib/types/next-auth.d.ts"
|
||||
],
|
||||
"types": ["@documenso/lib/types/next-auth.d.ts"],
|
||||
"strictNullChecks": true,
|
||||
"incremental": false
|
||||
},
|
||||
@@ -30,7 +24,5 @@
|
||||
".next/types/**/*.ts",
|
||||
".contentlayer/generated"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -23,6 +23,10 @@
|
||||
"lint-staged": "^14.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"turbo": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"apps/marketing": {
|
||||
@@ -87,7 +91,6 @@
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-pdf": "^7.1.1",
|
||||
"react-rnd": "^10.4.1",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"typescript": "5.1.6",
|
||||
@@ -12851,9 +12854,9 @@
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"node_modules/react-pdf": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.0.tgz",
|
||||
"integrity": "sha512-FRUqxGqh/l9TfND7T9ooW5ulZZt1tPYVwSEonFv3ImQFp3OkPM09BljchwEGHszP7P6h/Tt8js6v6d/T7GTC3A==",
|
||||
"version": "7.3.3",
|
||||
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.3.tgz",
|
||||
"integrity": "sha512-d7WAxcsjOogJfJ+I+zX/mdip3VjR1yq/yDa4hax4XbQVjbbbup6rqs4c8MGx0MLSnzob17TKp1t4CsNbDZ6GeQ==",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"make-cancellable-promise": "^1.3.1",
|
||||
@@ -16197,7 +16200,10 @@
|
||||
"date-fns": "^2.30.0",
|
||||
"framer-motion": "^10.12.8",
|
||||
"lucide-react": "^0.214.0",
|
||||
"next": "13.4.12",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
"react-day-picker": "^8.7.1",
|
||||
"react-pdf": "^7.3.3",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
"tailwindcss-animate": "^1.0.5"
|
||||
},
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"prepare": "husky install",
|
||||
"commitlint": "commitlint --edit"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8.6.0",
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.7.1",
|
||||
"@commitlint/config-conventional": "^17.7.0",
|
||||
|
||||
@@ -54,7 +54,10 @@
|
||||
"date-fns": "^2.30.0",
|
||||
"framer-motion": "^10.12.8",
|
||||
"lucide-react": "^0.214.0",
|
||||
"next": "13.4.12",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
"react-day-picker": "^8.7.1",
|
||||
"react-pdf": "^7.3.3",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
"tailwindcss-animate": "^1.0.5"
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Caveat } from 'next/font/google';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Check, ChevronsUpDown, Info } from 'lucide-react';
|
||||
import { nanoid } from 'nanoid';
|
||||
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 { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -22,20 +23,15 @@ import {
|
||||
} from '@documenso/ui/primitives/command';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||
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 {
|
||||
EditDocumentFormContainer,
|
||||
EditDocumentFormContainerActions,
|
||||
EditDocumentFormContainerContent,
|
||||
EditDocumentFormContainerFooter,
|
||||
EditDocumentFormContainerStep,
|
||||
} from './container';
|
||||
DocumentFlowFormContainer,
|
||||
DocumentFlowFormContainerActions,
|
||||
DocumentFlowFormContainerContent,
|
||||
DocumentFlowFormContainerFooter,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { FieldItem } from './field-item';
|
||||
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||
|
||||
@@ -58,18 +54,15 @@ export type AddFieldsFormProps = {
|
||||
document: Document;
|
||||
onContinue?: () => void;
|
||||
onGoBack?: () => void;
|
||||
onSubmit: (_data: TAddFieldsFormSchema) => void;
|
||||
};
|
||||
|
||||
export const AddFieldsFormPartial = ({
|
||||
recipients,
|
||||
fields,
|
||||
document,
|
||||
onContinue,
|
||||
onGoBack,
|
||||
onSubmit,
|
||||
}: AddFieldsFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -303,28 +296,6 @@ export const AddFieldsFormPartial = ({
|
||||
[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(() => {
|
||||
if (selectedField) {
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
@@ -357,8 +328,8 @@ export const AddFieldsFormPartial = ({
|
||||
}, [recipients]);
|
||||
|
||||
return (
|
||||
<EditDocumentFormContainer>
|
||||
<EditDocumentFormContainerContent
|
||||
<DocumentFlowFormContainer>
|
||||
<DocumentFlowFormContainerContent
|
||||
title="Add Fields"
|
||||
description="Add all relevant fields for each recipient."
|
||||
>
|
||||
@@ -560,18 +531,18 @@ export const AddFieldsFormPartial = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EditDocumentFormContainerContent>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<EditDocumentFormContainerFooter>
|
||||
<EditDocumentFormContainerStep title="Add Fields" step={2} maxStep={3} />
|
||||
<DocumentFlowFormContainerFooter>
|
||||
<DocumentFlowFormContainerStep title="Add Fields" step={2} maxStep={3} />
|
||||
|
||||
<EditDocumentFormContainerActions
|
||||
<DocumentFlowFormContainerActions
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
onGoNextClick={() => onFormSubmit()}
|
||||
onGoNextClick={() => handleSubmit(onSubmit)()}
|
||||
onGoBackClick={onGoBack}
|
||||
/>
|
||||
</EditDocumentFormContainerFooter>
|
||||
</EditDocumentFormContainer>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</DocumentFlowFormContainer>
|
||||
);
|
||||
};
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import React, { useId } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Plus, Trash } from 'lucide-react';
|
||||
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 { Button } from '@documenso/ui/primitives/button';
|
||||
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
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 {
|
||||
EditDocumentFormContainer,
|
||||
EditDocumentFormContainerActions,
|
||||
EditDocumentFormContainerContent,
|
||||
EditDocumentFormContainerFooter,
|
||||
EditDocumentFormContainerStep,
|
||||
} from './container';
|
||||
DocumentFlowFormContainer,
|
||||
DocumentFlowFormContainerActions,
|
||||
DocumentFlowFormContainerContent,
|
||||
DocumentFlowFormContainerFooter,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
|
||||
export type AddSignersFormProps = {
|
||||
recipients: Recipient[];
|
||||
@@ -33,17 +29,16 @@ export type AddSignersFormProps = {
|
||||
document: Document;
|
||||
onContinue?: () => void;
|
||||
onGoBack?: () => void;
|
||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||
};
|
||||
|
||||
export const AddSignersFormPartial = ({
|
||||
recipients,
|
||||
fields: _fields,
|
||||
document: document,
|
||||
onContinue,
|
||||
onGoBack,
|
||||
onSubmit,
|
||||
}: AddSignersFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
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 (
|
||||
<EditDocumentFormContainer onSubmit={onFormSubmit}>
|
||||
<EditDocumentFormContainerContent
|
||||
<DocumentFlowFormContainer onSubmit={handleSubmit(onSubmit)}>
|
||||
<DocumentFlowFormContainerContent
|
||||
title="Add Signers"
|
||||
description="Add the people who will sign the document."
|
||||
>
|
||||
@@ -229,18 +202,18 @@ export const AddSignersFormPartial = ({
|
||||
Add Signer
|
||||
</Button>
|
||||
</div>
|
||||
</EditDocumentFormContainerContent>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<EditDocumentFormContainerFooter>
|
||||
<EditDocumentFormContainerStep title="Add Signers" step={1} maxStep={3} />
|
||||
<DocumentFlowFormContainerFooter>
|
||||
<DocumentFlowFormContainerStep title="Add Signers" step={1} maxStep={3} />
|
||||
|
||||
<EditDocumentFormContainerActions
|
||||
<DocumentFlowFormContainerActions
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
onGoNextClick={() => onFormSubmit()}
|
||||
onGoNextClick={() => handleSubmit(onSubmit)()}
|
||||
onGoBackClick={onGoBack}
|
||||
/>
|
||||
</EditDocumentFormContainerFooter>
|
||||
</EditDocumentFormContainer>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</DocumentFlowFormContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Document, DocumentStatus, Field, Recipient } from '@documenso/prisma/client';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { FormErrorMessage } from '~/components/form/form-error-message';
|
||||
|
||||
import { completeDocument } from './add-subject.action';
|
||||
import { TAddSubjectFormSchema } from './add-subject.types';
|
||||
import {
|
||||
EditDocumentFormContainer,
|
||||
EditDocumentFormContainerActions,
|
||||
EditDocumentFormContainerContent,
|
||||
EditDocumentFormContainerFooter,
|
||||
EditDocumentFormContainerStep,
|
||||
} from './container';
|
||||
DocumentFlowFormContainer,
|
||||
DocumentFlowFormContainerActions,
|
||||
DocumentFlowFormContainerContent,
|
||||
DocumentFlowFormContainerFooter,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
|
||||
export type AddSubjectFormProps = {
|
||||
recipients: Recipient[];
|
||||
@@ -28,18 +24,16 @@ export type AddSubjectFormProps = {
|
||||
document: Document;
|
||||
onContinue?: () => void;
|
||||
onGoBack?: () => void;
|
||||
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||
};
|
||||
|
||||
export const AddSubjectFormPartial = ({
|
||||
recipients: _recipients,
|
||||
fields: _fields,
|
||||
document,
|
||||
onContinue,
|
||||
onGoBack,
|
||||
onSubmit,
|
||||
}: AddSubjectFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
register,
|
||||
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 (
|
||||
<EditDocumentFormContainer>
|
||||
<EditDocumentFormContainerContent
|
||||
<DocumentFlowFormContainer>
|
||||
<DocumentFlowFormContainerContent
|
||||
title="Add Subject"
|
||||
description="Add the subject and message you wish to send to signers."
|
||||
>
|
||||
@@ -151,19 +119,19 @@ export const AddSubjectFormPartial = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EditDocumentFormContainerContent>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<EditDocumentFormContainerFooter>
|
||||
<EditDocumentFormContainerStep title="Add Subject" step={3} maxStep={3} />
|
||||
<DocumentFlowFormContainerFooter>
|
||||
<DocumentFlowFormContainerStep title="Add Subject" step={3} maxStep={3} />
|
||||
|
||||
<EditDocumentFormContainerActions
|
||||
<DocumentFlowFormContainerActions
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
|
||||
onGoNextClick={() => onFormSubmit()}
|
||||
onGoNextClick={() => handleSubmit(onSubmit)()}
|
||||
onGoBackClick={onGoBack}
|
||||
/>
|
||||
</EditDocumentFormContainerFooter>
|
||||
</EditDocumentFormContainer>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</DocumentFlowFormContainer>
|
||||
);
|
||||
};
|
||||
@@ -7,16 +7,16 @@ import { Loader } from 'lucide-react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export type EditDocumentFormContainerProps = HTMLAttributes<HTMLFormElement> & {
|
||||
export type DocumentFlowFormContainerProps = HTMLAttributes<HTMLFormElement> & {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const EditDocumentFormContainer = ({
|
||||
export const DocumentFlowFormContainer = ({
|
||||
children,
|
||||
id = 'edit-document-form',
|
||||
className,
|
||||
...props
|
||||
}: EditDocumentFormContainerProps) => {
|
||||
}: DocumentFlowFormContainerProps) => {
|
||||
return (
|
||||
<form
|
||||
id={id}
|
||||
@@ -31,19 +31,19 @@ export const EditDocumentFormContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type EditDocumentFormContainerContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||
export type DocumentFlowFormContainerContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||
title: string;
|
||||
description: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const EditDocumentFormContainerContent = ({
|
||||
export const DocumentFlowFormContainerContent = ({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
...props
|
||||
}: EditDocumentFormContainerContentProps) => {
|
||||
}: DocumentFlowFormContainerContentProps) => {
|
||||
return (
|
||||
<div className={cn('flex flex-1 flex-col', className)} {...props}>
|
||||
<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;
|
||||
};
|
||||
|
||||
export const EditDocumentFormContainerFooter = ({
|
||||
export const DocumentFlowFormContainerFooter = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: EditDocumentFormContainerFooterProps) => {
|
||||
}: DocumentFlowFormContainerFooterProps) => {
|
||||
return (
|
||||
<div className={cn('mt-4 flex-shrink-0', className)} {...props}>
|
||||
{children}
|
||||
@@ -73,17 +73,17 @@ export const EditDocumentFormContainerFooter = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type EditDocumentFormContainerStepProps = {
|
||||
export type DocumentFlowFormContainerStepProps = {
|
||||
title: string;
|
||||
step: number;
|
||||
maxStep: number;
|
||||
};
|
||||
|
||||
export const EditDocumentFormContainerStep = ({
|
||||
export const DocumentFlowFormContainerStep = ({
|
||||
title,
|
||||
step,
|
||||
maxStep,
|
||||
}: EditDocumentFormContainerStepProps) => {
|
||||
}: DocumentFlowFormContainerStepProps) => {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
@@ -105,7 +105,7 @@ export const EditDocumentFormContainerStep = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type EditDocumentFormContainerActionsProps = {
|
||||
export type DocumentFlowFormContainerActionsProps = {
|
||||
canGoBack?: boolean;
|
||||
canGoNext?: boolean;
|
||||
goNextLabel?: string;
|
||||
@@ -116,7 +116,7 @@ export type EditDocumentFormContainerActionsProps = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const EditDocumentFormContainerActions = ({
|
||||
export const DocumentFlowFormContainerActions = ({
|
||||
canGoBack = true,
|
||||
canGoNext = true,
|
||||
goNextLabel = 'Continue',
|
||||
@@ -125,7 +125,7 @@ export const EditDocumentFormContainerActions = ({
|
||||
onGoNextClick,
|
||||
loading,
|
||||
disabled,
|
||||
}: EditDocumentFormContainerActionsProps) => {
|
||||
}: DocumentFlowFormContainerActionsProps) => {
|
||||
return (
|
||||
<div className="mt-4 flex gap-x-4">
|
||||
<Button
|
||||
@@ -6,14 +6,13 @@ import { Trash } from 'lucide-react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Rnd } from 'react-rnd';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
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 = TEditDocumentFormSchema['fields'][0];
|
||||
type Field = TDocumentFlowFormSchema['fields'][0];
|
||||
|
||||
export type FieldItemProps = {
|
||||
field: Field;
|
||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
export const ZEditDocumentFormSchema = z.object({
|
||||
export const ZDocumentFlowFormSchema = z.object({
|
||||
signers: z
|
||||
.array(
|
||||
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> = {
|
||||
[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 { Loader } from 'lucide-react';
|
||||
import { PDFDocumentProxy } from 'pdfjs-dist';
|
||||
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/TextLayer.css';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
type LoadedPDFDocument = pdfjs.PDFDocumentProxy;
|
||||
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||
|
||||
/**
|
||||
* This imports the worker from the `pdfjs-dist` package.
|
||||
*/
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.js',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `/pdf.worker.min.js`;
|
||||
|
||||
export type OnPDFViewerPageClick = (_event: {
|
||||
pageNumber: number;
|
||||
@@ -194,7 +194,6 @@ export const SignaturePad = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log({ defaultValue });
|
||||
if ($el.current && typeof defaultValue === 'string') {
|
||||
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