Compare commits
3 Commits
feat/add-d
...
minor/upda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3af26291c9 | ||
|
|
d1bc948f3c | ||
|
|
2b84636993 |
@@ -21,7 +21,6 @@
|
|||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.214.0",
|
"lucide-react": "^0.214.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"next": "13.4.1",
|
"next": "13.4.1",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"next-plausible": "^3.7.2",
|
"next-plausible": "^3.7.2",
|
||||||
@@ -33,7 +32,6 @@
|
|||||||
"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-pdf": "^7.1.1",
|
||||||
"react-rnd": "^10.4.1",
|
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
|
|||||||
3
apps/web/process-env.d.ts
vendored
3
apps/web/process-env.d.ts
vendored
@@ -11,5 +11,8 @@ declare namespace NodeJS {
|
|||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
|
||||||
NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED: string;
|
NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED: string;
|
||||||
|
|
||||||
|
NEXT_PRIVATE_GOOGLE_CLIENT_ID: string;
|
||||||
|
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { ChevronLeft, Loader } from 'lucide-react';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
|
|
||||||
<Link href="/documents" className="flex grow-0 items-center text-[#7AC455] hover:opacity-80">
|
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
|
||||||
Documents
|
|
||||||
</Link>
|
|
||||||
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
|
||||||
Loading Document...
|
|
||||||
</h1>
|
|
||||||
<div className="mt-8 grid min-h-[80vh] w-full grid-cols-12 gap-x-8">
|
|
||||||
<div className="dark:bg-background border-documenso col-span-7 rounded-xl border-2 bg-white/50 p-2 before:rounded-xl">
|
|
||||||
<div className="flex min-h-[80vh] flex-col items-center justify-center">
|
|
||||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">Loading document...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-background border-documenso col-span-5 rounded-xl border-2 before:rounded-xl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,6 @@ import { ChevronLeft } from 'lucide-react';
|
|||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
|
||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
|
||||||
|
|
||||||
import { EditDocumentForm } from '~/components/forms/edit-document';
|
import { EditDocumentForm } from '~/components/forms/edit-document';
|
||||||
|
|
||||||
@@ -36,22 +34,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
redirect('/documents');
|
redirect('/documents');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [recipients, fields] = await Promise.all([
|
|
||||||
await getRecipientsForDocument({
|
|
||||||
documentId,
|
|
||||||
userId: session.id,
|
|
||||||
}),
|
|
||||||
await getFieldsForDocument({
|
|
||||||
documentId,
|
|
||||||
userId: session.id,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<Link href="/documents" className="flex items-center text-[#7AC455] hover:opacity-80">
|
<Link href="/documents" className="flex items-center text-[#7AC455] hover:opacity-80">
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
Documents
|
Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
@@ -61,13 +48,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
{document.title}
|
{document.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<EditDocumentForm
|
<EditDocumentForm className="mt-8" document={document} user={session} />
|
||||||
className="mt-8"
|
|
||||||
document={document}
|
|
||||||
user={session}
|
|
||||||
recipients={recipients}
|
|
||||||
fields={fields}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Variants, motion } from 'framer-motion';
|
import { Variants, motion } from 'framer-motion';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@@ -91,6 +92,8 @@ export const DocumentDropzone = ({ className, onDrop, ...props }: DocumentDropzo
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={cn('flex', className)}
|
className={cn('flex', className)}
|
||||||
@@ -107,6 +110,7 @@ export const DocumentDropzone = ({ className, onDrop, ...props }: DocumentDropzo
|
|||||||
)}
|
)}
|
||||||
gradient={true}
|
gradient={true}
|
||||||
degrees={120}
|
degrees={120}
|
||||||
|
lightMode={theme === 'light'}
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -116,7 +120,6 @@ export const DocumentDropzone = ({ className, onDrop, ...props }: DocumentDropzo
|
|||||||
<motion.div
|
<motion.div
|
||||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||||
variants={DocumentDropzoneCardLeftVariants}
|
variants={DocumentDropzoneCardLeftVariants}
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
||||||
@@ -126,7 +129,6 @@ export const DocumentDropzone = ({ className, onDrop, ...props }: DocumentDropzo
|
|||||||
<motion.div
|
<motion.div
|
||||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||||
variants={DocumentDropzoneCardCenterVariants}
|
variants={DocumentDropzoneCardCenterVariants}
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<Plus
|
<Plus
|
||||||
strokeWidth="2px"
|
strokeWidth="2px"
|
||||||
@@ -137,7 +139,6 @@ export const DocumentDropzone = ({ className, onDrop, ...props }: DocumentDropzo
|
|||||||
<motion.div
|
<motion.div
|
||||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||||
variants={DocumentDropzoneCardRightVariants}
|
variants={DocumentDropzoneCardRightVariants}
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
||||||
|
|||||||
@@ -67,6 +67,16 @@ export const PDFViewer = ({ className, document, onPageClick, ...props }: PDFVie
|
|||||||
const pageX = event.clientX - left;
|
const pageX = event.clientX - left;
|
||||||
const pageY = event.clientY - top;
|
const pageY = event.clientY - top;
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
pageNumber,
|
||||||
|
numPages,
|
||||||
|
originalEvent: event,
|
||||||
|
pageHeight: height,
|
||||||
|
pageWidth: width,
|
||||||
|
pageX,
|
||||||
|
pageY,
|
||||||
|
});
|
||||||
|
|
||||||
if (onPageClick) {
|
if (onPageClick) {
|
||||||
onPageClick({
|
onPageClick({
|
||||||
pageNumber,
|
pageNumber,
|
||||||
@@ -110,10 +120,10 @@ export const PDFViewer = ({ className, document, onPageClick, ...props }: PDFVie
|
|||||||
onLoadSuccess={(d) => onDocumentLoaded(d)}
|
onLoadSuccess={(d) => onDocumentLoaded(d)}
|
||||||
externalLinkTarget="_blank"
|
externalLinkTarget="_blank"
|
||||||
loading={
|
loading={
|
||||||
<div className="dark:bg-background flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
|
<div className="flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
|
||||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
<Loader className="h-12 w-12 animate-spin text-slate-500" />
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">Loading document...</p>
|
<p className="mt-4 text-slate-500">Loading document...</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -127,8 +137,6 @@ export const PDFViewer = ({ className, document, onPageClick, ...props }: PDFVie
|
|||||||
<PDFPage
|
<PDFPage
|
||||||
pageNumber={i + 1}
|
pageNumber={i + 1}
|
||||||
width={width}
|
width={width}
|
||||||
renderAnnotationLayer={false}
|
|
||||||
renderTextLayer={false}
|
|
||||||
onClick={(e) => onDocumentPageClick(e, i + 1)}
|
onClick={(e) => onDocumentPageClick(e, i + 1)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export const PDF_VIEWER_CONTAINER_SELECTOR = '.react-pdf__Document';
|
|
||||||
export const PDF_VIEWER_PAGE_SELECTOR = '.react-pdf__Page';
|
|
||||||
@@ -41,16 +41,16 @@ export const Callout = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm" asChild>
|
<Link
|
||||||
<Link
|
href="https://github.com/documenso/documenso"
|
||||||
href="https://github.com/documenso/documenso"
|
target="_blank"
|
||||||
target="_blank"
|
onClick={() => event('view-github')}
|
||||||
onClick={() => event('view-github')}
|
>
|
||||||
>
|
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
||||||
<Github className="mr-2 h-5 w-5" />
|
<Github className="mr-2 h-5 w-5" />
|
||||||
Star on Github
|
Star on Github
|
||||||
</Link>
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -114,19 +114,12 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
|
||||||
variant="outline"
|
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
||||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="https://github.com/documenso/documenso"
|
|
||||||
onClick={() => event('view-github')}
|
|
||||||
>
|
|
||||||
<Github className="mr-2 h-5 w-5" />
|
<Github className="mr-2 h-5 w-5" />
|
||||||
Star on Github
|
Star on Github
|
||||||
</Link>
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -93,15 +93,14 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
|||||||
For small teams and individuals who need a simple solution
|
For small teams and individuals who need a simple solution
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button className="mt-6 rounded-full text-base">
|
<Link
|
||||||
<Link
|
href="https://github.com/documenso/documenso"
|
||||||
href="https://github.com/documenso/documenso"
|
target="_blank"
|
||||||
target="_blank"
|
className="mt-6"
|
||||||
onClick={() => event('view-github')}
|
onClick={() => event('view-github')}
|
||||||
>
|
>
|
||||||
View on Github
|
<Button className="rounded-full text-base">View on Github</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-col divide-y">
|
<div className="mt-8 flex w-full flex-col divide-y">
|
||||||
<p className="py-4 font-medium text-slate-900">Host your own instance</p>
|
<p className="py-4 font-medium text-slate-900">Host your own instance</p>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { FieldError } from 'react-hook-form';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
export type FormErrorMessageProps = {
|
export type FormErrorMessageProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
error: { message?: string } | undefined;
|
error: FieldError | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) => {
|
export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) => {
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useId, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { Document, Field, Recipient, User } from '@documenso/prisma/client';
|
import { Document, User } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/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';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
|
||||||
import { AddFieldsFormPartial } from './edit-document/add-fields';
|
import { AddFieldsFormPartial } from './edit-document/add-fields';
|
||||||
import { AddSignersFormPartial } from './edit-document/add-signers';
|
import { AddSignersFormPartial } from './edit-document/add-signers';
|
||||||
import { AddSubjectFormPartial } from './edit-document/add-subject';
|
|
||||||
import { TEditDocumentFormSchema, ZEditDocumentFormSchema } from './edit-document/types';
|
import { TEditDocumentFormSchema, ZEditDocumentFormSchema } from './edit-document/types';
|
||||||
|
|
||||||
const PDFViewer = dynamic(async () => import('~/components/(dashboard)/pdf-viewer/pdf-viewer'), {
|
const PDFViewer = dynamic(async () => import('~/components/(dashboard)/pdf-viewer/pdf-viewer'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => (
|
loading: () => (
|
||||||
<div className="dark:bg-background flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
|
<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" />
|
<Loader className="text-muted-foreground h-12 w-12 animate-spin" />
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">Loading document...</p>
|
<p className="text-muted-foreground mt-4">Loading document...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,143 +35,57 @@ export type EditDocumentFormProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
user: User;
|
||||||
document: Document;
|
document: Document;
|
||||||
recipients: Recipient[];
|
|
||||||
fields: Field[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditDocumentForm = ({
|
export const EditDocumentForm = ({ className, document, user: _user }: EditDocumentFormProps) => {
|
||||||
className,
|
const documentUrl = `data:application/pdf;base64,${document.document}`;
|
||||||
document,
|
|
||||||
recipients,
|
|
||||||
fields,
|
|
||||||
user: _user,
|
|
||||||
}: EditDocumentFormProps) => {
|
|
||||||
const initialId = useId();
|
|
||||||
|
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
const [nextStepLoading, setNextStepLoading] = useState(false);
|
|
||||||
|
|
||||||
const documentUrl = `data:application/pdf;base64,${document.document}`;
|
|
||||||
const defaultSigners =
|
|
||||||
recipients.length > 0
|
|
||||||
? recipients.map((recipient) => ({
|
|
||||||
nativeId: recipient.id,
|
|
||||||
formId: `${recipient.id}-${recipient.documentId}`,
|
|
||||||
name: recipient.name,
|
|
||||||
email: recipient.email,
|
|
||||||
}))
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
formId: initialId,
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultFields = fields.map((field) => ({
|
|
||||||
nativeId: field.id,
|
|
||||||
formId: `${field.id}-${field.documentId}`,
|
|
||||||
pageNumber: field.page,
|
|
||||||
type: field.type,
|
|
||||||
pageX: Number(field.positionX),
|
|
||||||
pageY: Number(field.positionY),
|
|
||||||
pageWidth: Number(field.width),
|
|
||||||
pageHeight: Number(field.height),
|
|
||||||
signerEmail: recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { mutateAsync: setRecipientsForDocument } =
|
|
||||||
trpc.document.setRecipientsForDocument.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: setFieldsForDocument } = trpc.document.setFieldsForDocument.useMutation();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
// handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
trigger,
|
formState: { errors, isSubmitting, isValid },
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
} = useForm<TEditDocumentFormSchema>({
|
} = useForm<TEditDocumentFormSchema>({
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
signers: defaultSigners,
|
signers: [
|
||||||
fields: defaultFields,
|
{
|
||||||
email: {
|
name: '',
|
||||||
subject: '',
|
email: '',
|
||||||
message: '',
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZEditDocumentFormSchema),
|
resolver: zodResolver(ZEditDocumentFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const signersFormValue = watch('signers');
|
const { theme } = useTheme();
|
||||||
const fieldsFormValue = watch('fields');
|
|
||||||
|
|
||||||
console.log({ state: watch(), errors });
|
|
||||||
|
|
||||||
const canGoBack = step > 0;
|
const canGoBack = step > 0;
|
||||||
const canGoNext = step < MAX_STEP;
|
const canGoNext = isValid && step < MAX_STEP;
|
||||||
|
|
||||||
const onGoBackClick = () => setStep((prev) => Math.max(0, prev - 1));
|
const onGoBackClick = () => setStep((prev) => Math.max(0, prev - 1));
|
||||||
const onGoNextClick = async () => {
|
const onGoNextClick = () => setStep((prev) => Math.min(MAX_STEP, prev + 1));
|
||||||
setNextStepLoading(true);
|
|
||||||
|
|
||||||
const passes = await trigger();
|
|
||||||
|
|
||||||
if (step === 0) {
|
|
||||||
await setRecipientsForDocument({
|
|
||||||
documentId: document.id,
|
|
||||||
recipients: signersFormValue.map((signer) => ({
|
|
||||||
id: signer.nativeId ?? undefined,
|
|
||||||
name: signer.name,
|
|
||||||
email: signer.email,
|
|
||||||
})),
|
|
||||||
}).catch((err: unknown) => console.error(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === 1) {
|
|
||||||
await setFieldsForDocument({
|
|
||||||
documentId: document.id,
|
|
||||||
fields: fieldsFormValue.map((field) => ({
|
|
||||||
id: field.nativeId ?? undefined,
|
|
||||||
type: field.type,
|
|
||||||
signerEmail: field.signerEmail,
|
|
||||||
pageNumber: field.pageNumber,
|
|
||||||
pageX: field.pageX,
|
|
||||||
pageY: field.pageY,
|
|
||||||
pageWidth: field.pageWidth,
|
|
||||||
pageHeight: field.pageHeight,
|
|
||||||
})),
|
|
||||||
}).catch((err: unknown) => console.error(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passes) {
|
|
||||||
setStep((prev) => Math.min(MAX_STEP, prev + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log({ passes });
|
|
||||||
|
|
||||||
setNextStepLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('grid w-full grid-cols-12 gap-x-8', className)}>
|
<div className={cn('grid w-full grid-cols-12 gap-x-8', className)}>
|
||||||
<Card className="col-span-7 rounded-xl before:rounded-xl" gradient>
|
<Card
|
||||||
|
className="col-span-7 rounded-xl before:rounded-xl"
|
||||||
|
gradient
|
||||||
|
lightMode={theme === 'light'}
|
||||||
|
>
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-2">
|
||||||
<PDFViewer document={documentUrl} />
|
<PDFViewer document={documentUrl} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="col-span-5">
|
<div className="relative col-span-5">
|
||||||
<form
|
<div className="dark:bg-background border-border sticky top-20 flex h-[calc(100vh-6rem)] max-h-screen flex-col rounded-xl border bg-[hsl(var(--widget))] px-4 py-6">
|
||||||
className="dark:bg-background border-border sticky top-20 flex h-[calc(100vh-6rem)] max-h-screen flex-col rounded-xl border bg-[hsl(var(--widget))] px-4 py-6"
|
|
||||||
onSubmit={handleSubmit(console.log)}
|
|
||||||
>
|
|
||||||
{step === 0 && (
|
{step === 0 && (
|
||||||
<AddSignersFormPartial
|
<AddSignersFormPartial
|
||||||
className="-mx-2 flex-1 overflow-y-hidden px-2"
|
className="-mx-2 flex-1 overflow-y-hidden px-2"
|
||||||
control={control}
|
control={control}
|
||||||
watch={watch}
|
|
||||||
errors={errors}
|
errors={errors}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
/>
|
/>
|
||||||
@@ -185,16 +98,7 @@ export const EditDocumentForm = ({
|
|||||||
watch={watch}
|
watch={watch}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
/>
|
theme={theme || 'dark'}
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 2 && (
|
|
||||||
<AddSubjectFormPartial
|
|
||||||
className="-mx-2 flex-1 overflow-y-hidden px-2"
|
|
||||||
control={control}
|
|
||||||
watch={watch}
|
|
||||||
errors={errors}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -214,7 +118,6 @@ export const EditDocumentForm = ({
|
|||||||
|
|
||||||
<div className="mt-4 flex gap-x-4">
|
<div className="mt-4 flex gap-x-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -224,27 +127,17 @@ export const EditDocumentForm = ({
|
|||||||
Go Back
|
Go Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{step < MAX_STEP && (
|
<Button
|
||||||
<Button
|
className="bg-documenso flex-1"
|
||||||
type="button"
|
size="lg"
|
||||||
className="bg-documenso flex-1"
|
disabled={!canGoNext}
|
||||||
size="lg"
|
onClick={onGoNextClick}
|
||||||
disabled={!canGoNext}
|
>
|
||||||
onClick={onGoNextClick}
|
Continue
|
||||||
>
|
</Button>
|
||||||
{nextStepLoading && <Loader className="mr-2 h-5 w-5 animate-spin" />}
|
|
||||||
Continue
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === MAX_STEP && (
|
|
||||||
<Button type="submit" className="bg-documenso flex-1" size="lg">
|
|
||||||
Complete
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Caveat } from 'next/font/google';
|
import { Caveat } from 'next/font/google';
|
||||||
|
|
||||||
import { Check, ChevronsUpDown } from 'lucide-react';
|
import { Check, ChevronsUpDown } from 'lucide-react';
|
||||||
import { nanoid } from 'nanoid';
|
import { Control, FieldErrors, UseFormWatch } from 'react-hook-form';
|
||||||
import { Control, FieldErrors, UseFormWatch, useFieldArray } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { FieldType } 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';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@@ -21,10 +19,7 @@ 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 { PDF_VIEWER_PAGE_SELECTOR } from '~/components/(dashboard)/pdf-viewer/types';
|
import { TEditDocumentFormSchema } from './types';
|
||||||
|
|
||||||
import { FieldItem } from './field-item';
|
|
||||||
import { FRIENDLY_FIELD_TYPE, TEditDocumentFormSchema } from './types';
|
|
||||||
|
|
||||||
const fontCaveat = Caveat({
|
const fontCaveat = Caveat({
|
||||||
weight: ['500'],
|
weight: ['500'],
|
||||||
@@ -33,285 +28,30 @@ const fontCaveat = Caveat({
|
|||||||
variable: '--font-caveat',
|
variable: '--font-caveat',
|
||||||
});
|
});
|
||||||
|
|
||||||
const DEFAULT_HEIGHT_PERCENT = 5;
|
|
||||||
const DEFAULT_WIDTH_PERCENT = 15;
|
|
||||||
|
|
||||||
const MIN_HEIGHT_PX = 60;
|
|
||||||
const MIN_WIDTH_PX = 200;
|
|
||||||
|
|
||||||
export type AddFieldsFormProps = {
|
export type AddFieldsFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
control: Control<TEditDocumentFormSchema>;
|
control: Control<TEditDocumentFormSchema>;
|
||||||
watch: UseFormWatch<TEditDocumentFormSchema>;
|
watch: UseFormWatch<TEditDocumentFormSchema>;
|
||||||
errors: FieldErrors<TEditDocumentFormSchema>;
|
errors: FieldErrors<TEditDocumentFormSchema>;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
|
theme: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddFieldsFormPartial = ({
|
export const AddFieldsFormPartial = ({
|
||||||
className,
|
className,
|
||||||
control: control,
|
control: _control,
|
||||||
watch,
|
watch,
|
||||||
errors: _errors,
|
errors: _errors,
|
||||||
isSubmitting: _isSubmitting,
|
isSubmitting: _isSubmitting,
|
||||||
|
theme,
|
||||||
}: AddFieldsFormProps) => {
|
}: AddFieldsFormProps) => {
|
||||||
const signers = watch('signers');
|
const signers = watch('signers');
|
||||||
const fields = watch('fields');
|
|
||||||
|
|
||||||
const { append, remove, update } = useFieldArray({
|
|
||||||
control,
|
|
||||||
name: 'fields',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [selectedSigner, setSelectedSigner] = useState(() => signers[0]);
|
const [selectedSigner, setSelectedSigner] = useState(() => signers[0]);
|
||||||
|
|
||||||
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
|
|
||||||
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [coords, setCoords] = useState({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fieldBounds = useRef({
|
|
||||||
height: 0,
|
|
||||||
width: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isWithinPageBounds = useCallback((event: MouseEvent) => {
|
|
||||||
if (!(event.target instanceof HTMLElement)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = event.target;
|
|
||||||
const $page =
|
|
||||||
target.closest(PDF_VIEWER_PAGE_SELECTOR) ?? target.querySelector(PDF_VIEWER_PAGE_SELECTOR);
|
|
||||||
|
|
||||||
if (!$page) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { top, left, height, width } = $page.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (event.clientY > top + height || event.clientY < top) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.clientX > left + width || event.clientX < left) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onMouseMove = useCallback(
|
|
||||||
(event: MouseEvent) => {
|
|
||||||
if (!isWithinPageBounds(event)) {
|
|
||||||
setVisible(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setVisible(true);
|
|
||||||
setCoords({
|
|
||||||
x: event.clientX - fieldBounds.current.width / 2,
|
|
||||||
y: event.clientY - fieldBounds.current.height / 2,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[isWithinPageBounds],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onMouseClick = useCallback(
|
|
||||||
(event: MouseEvent) => {
|
|
||||||
if (!selectedField) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(event.target instanceof HTMLElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = event.target;
|
|
||||||
|
|
||||||
const $page =
|
|
||||||
target.closest<HTMLElement>(PDF_VIEWER_PAGE_SELECTOR) ??
|
|
||||||
target.querySelector<HTMLElement>(PDF_VIEWER_PAGE_SELECTOR);
|
|
||||||
|
|
||||||
if (!$page || !isWithinPageBounds(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { height, width } = $page.getBoundingClientRect();
|
|
||||||
|
|
||||||
const top = $page.offsetTop;
|
|
||||||
const left = $page.offsetLeft;
|
|
||||||
|
|
||||||
const pageNumber = parseInt($page.getAttribute('data-page-number') ?? '1', 10);
|
|
||||||
|
|
||||||
// Calculate x and y as a percentage of the page width and height
|
|
||||||
let pageX = ((event.pageX - left) / width) * 100;
|
|
||||||
let pageY = ((event.pageY - top) / height) * 100;
|
|
||||||
|
|
||||||
// Get the bounds as a percentage of the page width and height
|
|
||||||
const fieldPageWidth = (fieldBounds.current.width / width) * 100;
|
|
||||||
const fieldPageHeight = (fieldBounds.current.height / height) * 100;
|
|
||||||
|
|
||||||
// And center it based on the bounds
|
|
||||||
pageX -= fieldPageWidth / 2;
|
|
||||||
pageY -= fieldPageHeight / 2;
|
|
||||||
|
|
||||||
append({
|
|
||||||
formId: nanoid(12),
|
|
||||||
type: selectedField,
|
|
||||||
pageNumber,
|
|
||||||
pageX,
|
|
||||||
pageY,
|
|
||||||
pageWidth: fieldPageWidth,
|
|
||||||
pageHeight: fieldPageHeight,
|
|
||||||
signerEmail: selectedSigner.email,
|
|
||||||
});
|
|
||||||
|
|
||||||
setVisible(false);
|
|
||||||
setSelectedField(null);
|
|
||||||
},
|
|
||||||
[append, isWithinPageBounds, selectedField, selectedSigner.email],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onFieldResize = useCallback(
|
|
||||||
(node: HTMLElement, index: number) => {
|
|
||||||
const field = fields[index];
|
|
||||||
|
|
||||||
const $page = document.querySelector<HTMLElement>(
|
|
||||||
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { height: pageHeight, width: pageWidth } = $page.getBoundingClientRect();
|
|
||||||
|
|
||||||
const pageTop = $page.offsetTop;
|
|
||||||
const pageLeft = $page.offsetLeft;
|
|
||||||
|
|
||||||
let { top: nodeTop, left: nodeLeft } = node.getBoundingClientRect();
|
|
||||||
const { height, width } = node.getBoundingClientRect();
|
|
||||||
|
|
||||||
nodeTop += window.scrollY;
|
|
||||||
nodeLeft += window.scrollX;
|
|
||||||
|
|
||||||
// Calculate width and height as a percentage of the page width and height
|
|
||||||
const newWidth = (width / pageWidth) * 100;
|
|
||||||
const newHeight = (height / pageHeight) * 100;
|
|
||||||
|
|
||||||
// Calculate the new position as a percentage of the page width and height
|
|
||||||
const newX = ((nodeLeft - pageLeft) / pageWidth) * 100;
|
|
||||||
const newY = ((nodeTop - pageTop) / pageHeight) * 100;
|
|
||||||
|
|
||||||
update(index, {
|
|
||||||
...field,
|
|
||||||
pageX: newX,
|
|
||||||
pageY: newY,
|
|
||||||
pageWidth: newWidth,
|
|
||||||
pageHeight: newHeight,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[fields, update],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onFieldMove = useCallback(
|
|
||||||
(node: HTMLElement, index: number) => {
|
|
||||||
const field = fields[index];
|
|
||||||
|
|
||||||
const $page = document.querySelector(
|
|
||||||
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$page || !($page instanceof HTMLElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { height: pageHeight, width: pageWidth } = $page.getBoundingClientRect();
|
|
||||||
|
|
||||||
const pageTop = $page.offsetTop;
|
|
||||||
const pageLeft = $page.offsetLeft;
|
|
||||||
|
|
||||||
let { top: nodeTop, left: nodeLeft } = node.getBoundingClientRect();
|
|
||||||
|
|
||||||
nodeTop += window.scrollY;
|
|
||||||
nodeLeft += window.scrollX;
|
|
||||||
|
|
||||||
// Calculate the new position as a percentage of the page width and height
|
|
||||||
const newX = ((nodeLeft - pageLeft) / pageWidth) * 100;
|
|
||||||
const newY = ((nodeTop - pageTop) / pageHeight) * 100;
|
|
||||||
|
|
||||||
update(index, {
|
|
||||||
...field,
|
|
||||||
pageX: newX,
|
|
||||||
pageY: newY,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[fields, update],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedField) {
|
|
||||||
window.addEventListener('mousemove', onMouseMove);
|
|
||||||
window.addEventListener('click', onMouseClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('mousemove', onMouseMove);
|
|
||||||
window.removeEventListener('click', onMouseClick);
|
|
||||||
};
|
|
||||||
}, [onMouseClick, onMouseMove, selectedField]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const $page = document.querySelector(PDF_VIEWER_PAGE_SELECTOR);
|
|
||||||
|
|
||||||
if (!$page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { height, width } = $page.getBoundingClientRect();
|
|
||||||
|
|
||||||
fieldBounds.current = {
|
|
||||||
height: Math.max(height * (DEFAULT_HEIGHT_PERCENT / 100), MIN_HEIGHT_PX),
|
|
||||||
width: Math.max(width * (DEFAULT_WIDTH_PERCENT / 100), MIN_WIDTH_PX),
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-col', className)}>
|
<div className={cn('flex flex-col', className)}>
|
||||||
{selectedField && visible && (
|
<h3 className="text-2xl font-semibold">Edit Document</h3>
|
||||||
<Card
|
|
||||||
className="border-primary pointer-events-none fixed z-50 cursor-pointer bg-white"
|
|
||||||
style={{
|
|
||||||
top: coords.y,
|
|
||||||
left: coords.x,
|
|
||||||
height: fieldBounds.current.height,
|
|
||||||
width: fieldBounds.current.width,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardContent className="text-foreground flex h-full w-full items-center justify-center p-2">
|
|
||||||
{FRIENDLY_FIELD_TYPE[selectedField]}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<FieldItem
|
|
||||||
key={index}
|
|
||||||
field={field}
|
|
||||||
disabled={selectedSigner.email !== field.signerEmail}
|
|
||||||
minHeight={fieldBounds.current.height}
|
|
||||||
minWidth={fieldBounds.current.width}
|
|
||||||
passive={visible && !!selectedField}
|
|
||||||
onResize={(options) => onFieldResize(options, index)}
|
|
||||||
onMove={(options) => onFieldMove(options, index)}
|
|
||||||
onRemove={() => remove(index)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<h3 className="text-2xl font-semibold">Add Fields</h3>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
Add all relevant fields for each recipient.
|
Add all relevant fields for each recipient.
|
||||||
@@ -322,7 +62,6 @@ export const AddFieldsFormPartial = ({
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className="bg-background text-muted-foreground justify-between font-normal"
|
className="bg-background text-muted-foreground justify-between font-normal"
|
||||||
@@ -348,7 +87,6 @@ export const AddFieldsFormPartial = ({
|
|||||||
{signers.map((signer, index) => (
|
{signers.map((signer, index) => (
|
||||||
<CommandItem key={index} onSelect={() => setSelectedSigner(signer)}>
|
<CommandItem key={index} onSelect={() => setSelectedSigner(signer)}>
|
||||||
<Check
|
<Check
|
||||||
aria-hidden={signer !== selectedSigner}
|
|
||||||
className={cn('mr-2 h-4 w-4', {
|
className={cn('mr-2 h-4 w-4', {
|
||||||
'opacity-0': signer !== selectedSigner,
|
'opacity-0': signer !== selectedSigner,
|
||||||
'opacity-100': signer === selectedSigner,
|
'opacity-100': signer === selectedSigner,
|
||||||
@@ -370,17 +108,15 @@ export const AddFieldsFormPartial = ({
|
|||||||
|
|
||||||
<div className="-mx-2 mt-8 flex-1 overflow-y-scroll px-2">
|
<div className="-mx-2 mt-8 flex-1 overflow-y-scroll px-2">
|
||||||
<div className="mt-4 grid grid-cols-2 gap-x-4 gap-y-8">
|
<div className="mt-4 grid grid-cols-2 gap-x-4 gap-y-8">
|
||||||
<button
|
<button className="group h-full w-full">
|
||||||
type="button"
|
<Card
|
||||||
className="group h-full w-full"
|
className="group-focus:border-documenso h-full w-full cursor-pointer"
|
||||||
onClick={() => setSelectedField(FieldType.SIGNATURE)}
|
lightMode={theme === 'light'}
|
||||||
data-selected={selectedField === FieldType.SIGNATURE ? true : undefined}
|
>
|
||||||
>
|
|
||||||
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer">
|
|
||||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground group-data-[selected]:text-foreground text-3xl font-medium',
|
'text-muted-foreground group-focus:text-foreground text-3xl font-medium',
|
||||||
fontCaveat.className,
|
fontCaveat.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -392,17 +128,15 @@ export const AddFieldsFormPartial = ({
|
|||||||
</Card>
|
</Card>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button className="group h-full w-full">
|
||||||
type="button"
|
<Card
|
||||||
className="group h-full w-full"
|
className="group-focus:border-documenso h-full w-full cursor-pointer"
|
||||||
onClick={() => setSelectedField(FieldType.EMAIL)}
|
lightMode={theme === 'light'}
|
||||||
data-selected={selectedField === FieldType.EMAIL ? true : undefined}
|
>
|
||||||
>
|
|
||||||
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer">
|
|
||||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
|
'text-muted-foreground group-focus:text-foreground text-xl font-medium',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{'Email'}
|
{'Email'}
|
||||||
@@ -413,17 +147,15 @@ export const AddFieldsFormPartial = ({
|
|||||||
</Card>
|
</Card>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button className="group h-full w-full">
|
||||||
type="button"
|
<Card
|
||||||
className="group h-full w-full"
|
className="group-focus:border-documenso h-full w-full cursor-pointer"
|
||||||
onClick={() => setSelectedField(FieldType.NAME)}
|
lightMode={theme === 'light'}
|
||||||
data-selected={selectedField === FieldType.NAME ? true : undefined}
|
>
|
||||||
>
|
|
||||||
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer">
|
|
||||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
|
'text-muted-foreground group-focus:text-foreground text-xl font-medium',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{'Name'}
|
{'Name'}
|
||||||
@@ -434,17 +166,15 @@ export const AddFieldsFormPartial = ({
|
|||||||
</Card>
|
</Card>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button className="group h-full w-full">
|
||||||
type="button"
|
<Card
|
||||||
className="group h-full w-full"
|
className="group-focus:border-documenso h-full w-full cursor-pointer"
|
||||||
onClick={() => setSelectedField(FieldType.DATE)}
|
lightMode={theme === 'light'}
|
||||||
data-selected={selectedField === FieldType.DATE ? true : undefined}
|
>
|
||||||
>
|
|
||||||
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer">
|
|
||||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
|
'text-muted-foreground group-focus:text-foreground text-xl font-medium',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{'Date'}
|
{'Date'}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
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 { Control, Controller, FieldErrors, useFieldArray } from 'react-hook-form';
|
||||||
import { Control, Controller, FieldErrors, UseFormWatch, useFieldArray } from 'react-hook-form';
|
|
||||||
|
|
||||||
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';
|
||||||
@@ -19,7 +16,6 @@ import { TEditDocumentFormSchema } from './types';
|
|||||||
export type AddSignersFormProps = {
|
export type AddSignersFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
control: Control<TEditDocumentFormSchema>;
|
control: Control<TEditDocumentFormSchema>;
|
||||||
watch: UseFormWatch<TEditDocumentFormSchema>;
|
|
||||||
errors: FieldErrors<TEditDocumentFormSchema>;
|
errors: FieldErrors<TEditDocumentFormSchema>;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
};
|
};
|
||||||
@@ -31,49 +27,14 @@ export const AddSignersFormPartial = ({
|
|||||||
isSubmitting,
|
isSubmitting,
|
||||||
}: AddSignersFormProps) => {
|
}: AddSignersFormProps) => {
|
||||||
const {
|
const {
|
||||||
append: appendSigner,
|
append,
|
||||||
fields: signers,
|
fields: signers,
|
||||||
remove: removeSigner,
|
remove,
|
||||||
} = useFieldArray({
|
} = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: 'signers',
|
name: 'signers',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { remove: removeField, fields: fields } = useFieldArray({
|
|
||||||
name: 'fields',
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onAddSigner = () => {
|
|
||||||
appendSigner({
|
|
||||||
formId: nanoid(12),
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemoveSigner = (index: number) => {
|
|
||||||
const signer = signers[index];
|
|
||||||
|
|
||||||
removeSigner(index);
|
|
||||||
|
|
||||||
const fieldsToRemove: number[] = [];
|
|
||||||
|
|
||||||
fields.forEach((field, fieldIndex) => {
|
|
||||||
if (field.signerEmail === signer.email) {
|
|
||||||
fieldsToRemove.push(fieldIndex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
removeField(fieldsToRemove);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (event.key === 'Enter' && event.target instanceof HTMLInputElement) {
|
|
||||||
onAddSigner();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-col', className)}>
|
<div className={cn('flex flex-col', className)}>
|
||||||
<h3 className="text-foreground text-2xl font-semibold">Add Signers</h3>
|
<h3 className="text-foreground text-2xl font-semibold">Add Signers</h3>
|
||||||
@@ -84,27 +45,23 @@ export const AddSignersFormPartial = ({
|
|||||||
|
|
||||||
<hr className="border-border mb-8 mt-4" />
|
<hr className="border-border mb-8 mt-4" />
|
||||||
|
|
||||||
<div className="-mx-2 flex flex-1 flex-col overflow-y-auto px-2">
|
<div className="-mx-2 flex flex-1 flex-col overflow-y-scroll px-2">
|
||||||
<div className="flex w-full flex-col gap-y-4">
|
<div className="flex w-full flex-col gap-y-4">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{signers.map((signer, index) => (
|
{signers.map((field, index) => (
|
||||||
<motion.div key={signer.formId} className="flex flex-wrap items-end gap-x-4">
|
<motion.div key={field.id} className="flex flex-wrap items-end gap-x-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label htmlFor={`signer-${signer.formId}-email`}>
|
<Label htmlFor={`signer-${index}-email`}>Email</Label>
|
||||||
Email
|
|
||||||
<span className="text-destructive ml-1 inline-block font-medium">*</span>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`signers.${index}.email`}
|
name={`signers.${index}.email`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
id={`signer-${signer.formId}-email`}
|
id={`signer-${index}-email`}
|
||||||
type="email"
|
type="email"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -112,18 +69,17 @@ export const AddSignersFormPartial = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label htmlFor={`signer-${signer.formId}-name`}>Name</Label>
|
<Label htmlFor={`signer-${index}-name`}>Name</Label>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`signers.${index}.name`}
|
name={`signers.${index}.name`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
id={`signer-${signer.formId}-name`}
|
id={`signer-${index}-name`}
|
||||||
type="text"
|
type="text"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -133,9 +89,9 @@ export const AddSignersFormPartial = ({
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-10 w-10 items-center justify-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
|
className="inline-flex h-10 w-10 items-center justify-center text-slate-500 hover:opacity-80"
|
||||||
disabled={isSubmitting || signers.length === 1}
|
disabled={isSubmitting}
|
||||||
onClick={() => onRemoveSigner(index)}
|
onClick={() => remove(index)}
|
||||||
>
|
>
|
||||||
<Trash className="h-5 w-5" />
|
<Trash className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -150,10 +106,17 @@ export const AddSignersFormPartial = ({
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormErrorMessage className="mt-2" error={errors.signers} />
|
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Button type="button" disabled={isSubmitting} onClick={() => onAddSigner()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
email: '',
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
||||||
Add Signer
|
Add Signer
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Control, Controller, FieldErrors, UseFormWatch } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
|
||||||
import { Textarea } from '@documenso/ui/primitives/textarea';
|
|
||||||
|
|
||||||
import { FormErrorMessage } from '~/components/form/form-error-message';
|
|
||||||
|
|
||||||
import { TEditDocumentFormSchema } from './types';
|
|
||||||
|
|
||||||
export type AddSubjectFormProps = {
|
|
||||||
className?: string;
|
|
||||||
control: Control<TEditDocumentFormSchema>;
|
|
||||||
watch: UseFormWatch<TEditDocumentFormSchema>;
|
|
||||||
errors: FieldErrors<TEditDocumentFormSchema>;
|
|
||||||
isSubmitting: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AddSubjectFormPartial = ({
|
|
||||||
className,
|
|
||||||
control,
|
|
||||||
errors,
|
|
||||||
isSubmitting,
|
|
||||||
}: AddSubjectFormProps) => {
|
|
||||||
return (
|
|
||||||
<div className={cn('flex flex-col', className)}>
|
|
||||||
<h3 className="text-foreground text-2xl font-semibold">Add Subject</h3>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
|
||||||
Add the subject and message you wish to send to signers.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<hr className="border-border mb-8 mt-4" />
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="subject">
|
|
||||||
Subject <span className="text-muted-foreground">(Optional)</span>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="email.subject"
|
|
||||||
render={({ field }) => (
|
|
||||||
<Input
|
|
||||||
id="subject"
|
|
||||||
// placeholder="Subject"
|
|
||||||
className="bg-background mt-2"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormErrorMessage className="mt-2" errors={errors} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="message">
|
|
||||||
Message <span className="text-muted-foreground">(Optional)</span>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="email.message"
|
|
||||||
render={({ field }) => (
|
|
||||||
<Textarea
|
|
||||||
id="message"
|
|
||||||
className="bg-background mt-2 h-32 resize-none"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormErrorMessage className="mt-2" errors={errors} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
You can use the following variables in your message:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul className="mt-2 flex list-inside list-disc flex-col gap-y-2 text-sm">
|
|
||||||
<li className="text-muted-foreground">
|
|
||||||
<code className="text-muted-foreground bg-muted-foreground/20 rounded p-1 text-sm">
|
|
||||||
{'{signer.name}'}
|
|
||||||
</code>{' '}
|
|
||||||
- The signer's name
|
|
||||||
</li>
|
|
||||||
<li className="text-muted-foreground">
|
|
||||||
<code className="text-muted-foreground bg-muted-foreground/20 rounded p-1 text-sm">
|
|
||||||
{'{signer.email}'}
|
|
||||||
</code>{' '}
|
|
||||||
- The signer's email
|
|
||||||
</li>
|
|
||||||
<li className="text-muted-foreground">
|
|
||||||
<code className="text-muted-foreground bg-muted-foreground/20 rounded p-1 text-sm">
|
|
||||||
{'{document.name}'}
|
|
||||||
</code>{' '}
|
|
||||||
- The document's name
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { X } from 'lucide-react';
|
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { Rnd } from 'react-rnd';
|
|
||||||
|
|
||||||
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, TEditDocumentFormSchema } from './types';
|
|
||||||
|
|
||||||
type Field = TEditDocumentFormSchema['fields'][0];
|
|
||||||
|
|
||||||
export type FieldItemProps = {
|
|
||||||
field: Field;
|
|
||||||
passive?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
minHeight?: number;
|
|
||||||
minWidth?: number;
|
|
||||||
onResize?: (_node: HTMLElement) => void;
|
|
||||||
onMove?: (_node: HTMLElement) => void;
|
|
||||||
onRemove?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FieldItem = ({
|
|
||||||
field,
|
|
||||||
passive,
|
|
||||||
disabled,
|
|
||||||
minHeight,
|
|
||||||
minWidth,
|
|
||||||
onResize,
|
|
||||||
onMove,
|
|
||||||
onRemove,
|
|
||||||
}: FieldItemProps) => {
|
|
||||||
const [active, setActive] = useState(false);
|
|
||||||
const [coords, setCoords] = useState({
|
|
||||||
pageX: 0,
|
|
||||||
pageY: 0,
|
|
||||||
pageHeight: 0,
|
|
||||||
pageWidth: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const calculateCoords = useCallback(() => {
|
|
||||||
const $page = document.querySelector<HTMLElement>(
|
|
||||||
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { height, width } = $page.getBoundingClientRect();
|
|
||||||
|
|
||||||
const top = $page.offsetTop;
|
|
||||||
const left = $page.offsetLeft;
|
|
||||||
|
|
||||||
// X and Y are percentages of the page's height and width
|
|
||||||
const pageX = (field.pageX / 100) * width + left;
|
|
||||||
const pageY = (field.pageY / 100) * height + top;
|
|
||||||
|
|
||||||
const pageHeight = (field.pageHeight / 100) * height;
|
|
||||||
const pageWidth = (field.pageWidth / 100) * width;
|
|
||||||
|
|
||||||
setCoords({
|
|
||||||
pageX: pageX,
|
|
||||||
pageY: pageY,
|
|
||||||
pageHeight: pageHeight,
|
|
||||||
pageWidth: pageWidth,
|
|
||||||
});
|
|
||||||
}, [field.pageHeight, field.pageNumber, field.pageWidth, field.pageX, field.pageY]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
calculateCoords();
|
|
||||||
}, [calculateCoords]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onResize = () => {
|
|
||||||
calculateCoords();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('resize', onResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', onResize);
|
|
||||||
};
|
|
||||||
}, [calculateCoords]);
|
|
||||||
|
|
||||||
return createPortal(
|
|
||||||
<Rnd
|
|
||||||
key={coords.pageX + coords.pageY + coords.pageHeight + coords.pageWidth}
|
|
||||||
className={cn('absolute z-20', {
|
|
||||||
'pointer-events-none': passive,
|
|
||||||
'pointer-events-none opacity-75': disabled,
|
|
||||||
})}
|
|
||||||
minHeight={minHeight}
|
|
||||||
minWidth={minWidth}
|
|
||||||
default={{
|
|
||||||
x: coords.pageX,
|
|
||||||
y: coords.pageY,
|
|
||||||
height: coords.pageHeight,
|
|
||||||
width: coords.pageWidth,
|
|
||||||
}}
|
|
||||||
bounds={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`}
|
|
||||||
onDragStart={() => setActive(true)}
|
|
||||||
onResizeStart={() => setActive(true)}
|
|
||||||
onResizeStop={(_e, _d, ref) => {
|
|
||||||
setActive(false);
|
|
||||||
onResize?.(ref);
|
|
||||||
}}
|
|
||||||
onDragStop={(_e, d) => {
|
|
||||||
setActive(false);
|
|
||||||
onMove?.(d.node);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!disabled && (
|
|
||||||
<button
|
|
||||||
className="bg-destructive absolute -right-2 -top-2 z-[9999] flex h-5 w-5 items-center justify-center rounded-full"
|
|
||||||
onClick={() => onRemove?.()}
|
|
||||||
>
|
|
||||||
<X className="text-destructive-foreground h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card
|
|
||||||
className={cn('hover:border-primary/50 h-full w-full bg-white', {
|
|
||||||
'border-primary hover:border-primary': active,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<CardContent
|
|
||||||
className={cn(
|
|
||||||
'text-foreground flex h-full w-full flex-col items-center justify-center p-2',
|
|
||||||
{
|
|
||||||
'text-muted-foreground/50': disabled,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{FRIENDLY_FIELD_TYPE[field.type]}
|
|
||||||
|
|
||||||
<p className="text-muted-foreground/50 w-full truncate text-center text-xs">
|
|
||||||
{field.signerEmail}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Rnd>,
|
|
||||||
document.body,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import React, { createContext, useRef } from 'react';
|
|
||||||
|
|
||||||
import { OnPDFViewerPageClick } from '~/components/(dashboard)/pdf-viewer/pdf-viewer';
|
|
||||||
|
|
||||||
type EditFormContextValue = {
|
|
||||||
firePageClickEvent: OnPDFViewerPageClick;
|
|
||||||
registerPageClickHandler: (_handler: OnPDFViewerPageClick) => void;
|
|
||||||
unregisterPageClickHandler: (_handler: OnPDFViewerPageClick) => void;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
const EditFormContext = createContext<EditFormContextValue>(null);
|
|
||||||
|
|
||||||
export type EditFormProviderProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useEditForm = () => {
|
|
||||||
const context = React.useContext(EditFormContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useEditForm must be used within a EditFormProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditFormProvider = ({ children }: EditFormProviderProps) => {
|
|
||||||
const handlers = useRef(new Set<OnPDFViewerPageClick>());
|
|
||||||
|
|
||||||
const firePageClickEvent: OnPDFViewerPageClick = (event) => {
|
|
||||||
handlers.current.forEach((handler) => handler(event));
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerPageClickHandler = (handler: OnPDFViewerPageClick) => {
|
|
||||||
handlers.current.add(handler);
|
|
||||||
};
|
|
||||||
|
|
||||||
const unregisterPageClickHandler = (handler: OnPDFViewerPageClick) => {
|
|
||||||
handlers.current.delete(handler);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EditFormContext.Provider
|
|
||||||
value={{
|
|
||||||
firePageClickEvent,
|
|
||||||
registerPageClickHandler,
|
|
||||||
unregisterPageClickHandler,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</EditFormContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,49 +1,13 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
export const ZEditDocumentFormSchema = z.object({
|
export const ZEditDocumentFormSchema = z.object({
|
||||||
signers: z
|
signers: z.array(
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
formId: z.string().min(1),
|
|
||||||
nativeId: z.number().optional(),
|
|
||||||
email: z.string().min(1).email(),
|
|
||||||
name: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.refine((signers) => {
|
|
||||||
const emails = signers.map((signer) => signer.email);
|
|
||||||
return new Set(emails).size === emails.length;
|
|
||||||
}, 'Signers must have unique emails'),
|
|
||||||
|
|
||||||
fields: z.array(
|
|
||||||
z.object({
|
z.object({
|
||||||
formId: z.string().min(1),
|
id: z.number().optional(),
|
||||||
nativeId: z.number().optional(),
|
email: z.string().min(1).email(),
|
||||||
type: z.nativeEnum(FieldType),
|
name: z.string(),
|
||||||
signerEmail: z.string().min(1),
|
|
||||||
pageNumber: z.number().min(1),
|
|
||||||
pageX: z.number().min(0),
|
|
||||||
pageY: z.number().min(0),
|
|
||||||
pageWidth: z.number().min(0),
|
|
||||||
pageHeight: z.number().min(0),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
||||||
email: z.object({
|
|
||||||
subject: z.string(),
|
|
||||||
message: z.string(),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TEditDocumentFormSchema = z.infer<typeof ZEditDocumentFormSchema>;
|
export type TEditDocumentFormSchema = z.infer<typeof ZEditDocumentFormSchema>;
|
||||||
|
|
||||||
export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
|
|
||||||
[FieldType.SIGNATURE]: 'Signature',
|
|
||||||
[FieldType.FREE_SIGNATURE]: 'Free Signature',
|
|
||||||
[FieldType.TEXT]: 'Text',
|
|
||||||
[FieldType.DATE]: 'Date',
|
|
||||||
[FieldType.EMAIL]: 'Email',
|
|
||||||
[FieldType.NAME]: 'Name',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
|
|
||||||
const onSignInWithGoogleClick = async () => {
|
const onSignInWithGoogleClick = async () => {
|
||||||
try {
|
try {
|
||||||
// await signIn('google', { callbackUrl: '/dashboard' });
|
await signIn('google', { callbackUrl: '/dashboard' });
|
||||||
throw new Error('Not implemented');
|
// throw new Error('Not implemented');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: 'An unknown error occurred',
|
title: 'An unknown error occurred',
|
||||||
|
|||||||
146
package-lock.json
generated
146
package-lock.json
generated
@@ -64,7 +64,6 @@
|
|||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.214.0",
|
"lucide-react": "^0.214.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"next": "13.4.1",
|
"next": "13.4.1",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"next-plausible": "^3.7.2",
|
"next-plausible": "^3.7.2",
|
||||||
@@ -76,7 +75,6 @@
|
|||||||
"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-pdf": "^7.1.1",
|
||||||
"react-rnd": "^10.4.1",
|
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
@@ -87,23 +85,6 @@
|
|||||||
"@types/react-dom": "18.2.4"
|
"@types/react-dom": "18.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/web/node_modules/nanoid": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bin": {
|
|
||||||
"nanoid": "bin/nanoid.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^14 || ^16 || >=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
@@ -4218,11 +4199,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
|
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
|
||||||
},
|
},
|
||||||
"node_modules/fast-memoize": {
|
|
||||||
"version": "2.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz",
|
|
||||||
"integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="
|
|
||||||
},
|
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||||
@@ -6630,19 +6606,6 @@
|
|||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-draggable": {
|
|
||||||
"version": "4.4.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz",
|
|
||||||
"integrity": "sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==",
|
|
||||||
"dependencies": {
|
|
||||||
"clsx": "^1.1.1",
|
|
||||||
"prop-types": "^15.8.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">= 16.3.0",
|
|
||||||
"react-dom": ">= 16.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-dropzone": {
|
"node_modules/react-dropzone": {
|
||||||
"version": "14.2.3",
|
"version": "14.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
|
||||||
@@ -6754,37 +6717,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-rnd": {
|
|
||||||
"version": "10.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.4.1.tgz",
|
|
||||||
"integrity": "sha512-0m887AjQZr6p2ADLNnipquqsDq4XJu/uqVqI3zuoGD19tRm6uB83HmZWydtkilNp5EWsOHbLGF4IjWMdd5du8Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"re-resizable": "6.9.6",
|
|
||||||
"react-draggable": "4.4.5",
|
|
||||||
"tslib": "2.3.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16.3.0",
|
|
||||||
"react-dom": ">=16.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-rnd/node_modules/re-resizable": {
|
|
||||||
"version": "6.9.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz",
|
|
||||||
"integrity": "sha512-0xYKS5+Z0zk+vICQlcZW+g54CcJTTmHluA7JUUgvERDxnKAnytylcyPsA+BSFi759s5hPlHmBRegFrwXs2FuBQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"fast-memoize": "^2.5.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
|
||||||
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-rnd/node_modules/tslib": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
|
||||||
},
|
|
||||||
"node_modules/react-ssr-prepass": {
|
"node_modules/react-ssr-prepass": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz",
|
||||||
@@ -8410,7 +8342,6 @@
|
|||||||
"@pdf-lib/fontkit": "^1.1.1",
|
"@pdf-lib/fontkit": "^1.1.1",
|
||||||
"@upstash/redis": "^1.20.6",
|
"@upstash/redis": "^1.20.6",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"next": "13.4.1",
|
"next": "13.4.1",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
@@ -8420,23 +8351,6 @@
|
|||||||
"@types/bcrypt": "^5.0.0"
|
"@types/bcrypt": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/lib/node_modules/nanoid": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bin": {
|
|
||||||
"nanoid": "bin/nanoid.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^14 || ^16 || >=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/prettier-config": {
|
"packages/prettier-config": {
|
||||||
"name": "@documenso/prettier-config",
|
"name": "@documenso/prettier-config",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -8476,8 +8390,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
|
||||||
"@documenso/prisma": "*",
|
|
||||||
"@tanstack/react-query": "^4.29.5",
|
"@tanstack/react-query": "^4.29.5",
|
||||||
"@trpc/client": "^10.25.1",
|
"@trpc/client": "^10.25.1",
|
||||||
"@trpc/next": "^10.25.1",
|
"@trpc/next": "^10.25.1",
|
||||||
@@ -8930,18 +8842,10 @@
|
|||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@upstash/redis": "^1.20.6",
|
"@upstash/redis": "^1.20.6",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"next": "13.4.1",
|
"next": "13.4.1",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"stripe": "^12.7.0"
|
"stripe": "^12.7.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"nanoid": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@documenso/marketing": {
|
"@documenso/marketing": {
|
||||||
@@ -8999,8 +8903,6 @@
|
|||||||
"@documenso/trpc": {
|
"@documenso/trpc": {
|
||||||
"version": "file:packages/trpc",
|
"version": "file:packages/trpc",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@documenso/lib": "*",
|
|
||||||
"@documenso/prisma": "*",
|
|
||||||
"@tanstack/react-query": "^4.29.5",
|
"@tanstack/react-query": "^4.29.5",
|
||||||
"@trpc/client": "^10.25.1",
|
"@trpc/client": "^10.25.1",
|
||||||
"@trpc/next": "^10.25.1",
|
"@trpc/next": "^10.25.1",
|
||||||
@@ -9076,7 +8978,6 @@
|
|||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.214.0",
|
"lucide-react": "^0.214.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"next": "13.4.1",
|
"next": "13.4.1",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"next-plausible": "^3.7.2",
|
"next-plausible": "^3.7.2",
|
||||||
@@ -9088,16 +8989,8 @@
|
|||||||
"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-pdf": "^7.1.1",
|
||||||
"react-rnd": "^10.4.1",
|
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"nanoid": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@emotion/is-prop-valid": {
|
"@emotion/is-prop-valid": {
|
||||||
@@ -11800,11 +11693,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
|
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
|
||||||
},
|
},
|
||||||
"fast-memoize": {
|
|
||||||
"version": "2.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz",
|
|
||||||
"integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="
|
|
||||||
},
|
|
||||||
"fastq": {
|
"fastq": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||||
@@ -13348,15 +13236,6 @@
|
|||||||
"scheduler": "^0.23.0"
|
"scheduler": "^0.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-draggable": {
|
|
||||||
"version": "4.4.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz",
|
|
||||||
"integrity": "sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==",
|
|
||||||
"requires": {
|
|
||||||
"clsx": "^1.1.1",
|
|
||||||
"prop-types": "^15.8.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"react-dropzone": {
|
"react-dropzone": {
|
||||||
"version": "14.2.3",
|
"version": "14.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
|
||||||
@@ -13420,31 +13299,6 @@
|
|||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-rnd": {
|
|
||||||
"version": "10.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.4.1.tgz",
|
|
||||||
"integrity": "sha512-0m887AjQZr6p2ADLNnipquqsDq4XJu/uqVqI3zuoGD19tRm6uB83HmZWydtkilNp5EWsOHbLGF4IjWMdd5du8Q==",
|
|
||||||
"requires": {
|
|
||||||
"re-resizable": "6.9.6",
|
|
||||||
"react-draggable": "4.4.5",
|
|
||||||
"tslib": "2.3.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"re-resizable": {
|
|
||||||
"version": "6.9.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz",
|
|
||||||
"integrity": "sha512-0xYKS5+Z0zk+vICQlcZW+g54CcJTTmHluA7JUUgvERDxnKAnytylcyPsA+BSFi759s5hPlHmBRegFrwXs2FuBQ==",
|
|
||||||
"requires": {
|
|
||||||
"fast-memoize": "^2.5.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tslib": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"react-ssr-prepass": {
|
"react-ssr-prepass": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
||||||
import { compare } from 'bcrypt';
|
import { compare } from 'bcrypt';
|
||||||
import { AuthOptions, User } from 'next-auth';
|
import { AuthOptions, Session, User } from 'next-auth';
|
||||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||||
|
import GoogleProvider from 'next-auth/providers/google';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
@@ -40,19 +41,62 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(user.id) as any,
|
id: String(user.id),
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
image: '',
|
|
||||||
} satisfies User;
|
} satisfies User;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
GoogleProvider({
|
||||||
|
clientId: process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID ?? '',
|
||||||
|
clientSecret: process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET ?? '',
|
||||||
|
allowDangerousEmailAccountLinking: true,
|
||||||
|
profile(profile) {
|
||||||
|
return {
|
||||||
|
id: profile.sub as any,
|
||||||
|
name: profile.name,
|
||||||
|
email: profile.email,
|
||||||
|
image: profile.picture,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
// callbacks: {
|
callbacks: {
|
||||||
// jwt: async ({ token, user: _user }) => {
|
async jwt({ token, user }) {
|
||||||
// return {
|
const dbUser = await prisma.user.findFirst({
|
||||||
// ...token,
|
where: {
|
||||||
// };
|
email: token.email as string,
|
||||||
// },
|
},
|
||||||
// },
|
});
|
||||||
|
|
||||||
|
if (!dbUser) {
|
||||||
|
if (user) {
|
||||||
|
token.id = user?.id;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: dbUser.id,
|
||||||
|
name: dbUser.name,
|
||||||
|
email: dbUser.email,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async session({ token, session }) {
|
||||||
|
if (token) {
|
||||||
|
const documensoSession = {
|
||||||
|
...session,
|
||||||
|
user: {
|
||||||
|
id: Number(token.id),
|
||||||
|
name: token.name,
|
||||||
|
email: token.email,
|
||||||
|
image: token.image,
|
||||||
|
},
|
||||||
|
} as Session;
|
||||||
|
|
||||||
|
return documensoSession;
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
"@upstash/redis": "^1.20.6",
|
"@upstash/redis": "^1.20.6",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"next": "13.4.1",
|
"next": "13.4.1",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"stripe": "^12.7.0"
|
"stripe": "^12.7.0"
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
export interface GetFieldsForDocumentOptions {
|
|
||||||
documentId: number;
|
|
||||||
userId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForDocumentOptions) => {
|
|
||||||
const fields = await prisma.field.findMany({
|
|
||||||
where: {
|
|
||||||
documentId,
|
|
||||||
Document: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return fields;
|
|
||||||
};
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
export interface SetFieldsForDocumentOptions {
|
|
||||||
userId: number;
|
|
||||||
documentId: number;
|
|
||||||
fields: {
|
|
||||||
id?: number | null;
|
|
||||||
signerEmail: string;
|
|
||||||
pageNumber: number;
|
|
||||||
pageX: number;
|
|
||||||
pageY: number;
|
|
||||||
pageWidth: number;
|
|
||||||
pageHeight: number;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setFieldsForDocument = async ({
|
|
||||||
userId,
|
|
||||||
documentId,
|
|
||||||
fields,
|
|
||||||
}: SetFieldsForDocumentOptions) => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new Error('Document not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingFields = await prisma.field.findMany({
|
|
||||||
where: {
|
|
||||||
documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const removedFields = existingFields.filter(
|
|
||||||
(existingField) =>
|
|
||||||
!fields.find(
|
|
||||||
(field) =>
|
|
||||||
field.id === existingField.id || field.signerEmail === existingField.Recipient?.email,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const linkedFields = fields.map((field) => {
|
|
||||||
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
...existing,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const field of linkedFields) {
|
|
||||||
if (
|
|
||||||
field.Recipient?.sendStatus === SendStatus.SENT ||
|
|
||||||
field.Recipient?.signingStatus === SigningStatus.SIGNED
|
|
||||||
) {
|
|
||||||
throw new Error('Cannot modify fields after sending');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistedFields = await prisma.$transaction(
|
|
||||||
linkedFields.map((field) =>
|
|
||||||
field.id
|
|
||||||
? prisma.field.update({
|
|
||||||
where: {
|
|
||||||
id: field.id,
|
|
||||||
recipientId: field.recipientId,
|
|
||||||
documentId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: field.type,
|
|
||||||
page: field.pageNumber,
|
|
||||||
positionX: field.pageX,
|
|
||||||
positionY: field.pageY,
|
|
||||||
width: field.pageWidth,
|
|
||||||
height: field.pageHeight,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: prisma.field.create({
|
|
||||||
data: {
|
|
||||||
type: field.type!,
|
|
||||||
page: field.pageNumber,
|
|
||||||
positionX: field.pageX,
|
|
||||||
positionY: field.pageY,
|
|
||||||
width: field.pageWidth,
|
|
||||||
height: field.pageHeight,
|
|
||||||
customText: '',
|
|
||||||
inserted: false,
|
|
||||||
|
|
||||||
Document: {
|
|
||||||
connect: {
|
|
||||||
id: document.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Recipient: {
|
|
||||||
connect: {
|
|
||||||
documentId_email: {
|
|
||||||
documentId: document.id,
|
|
||||||
email: field.signerEmail,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (removedFields.length > 0) {
|
|
||||||
await prisma.field.deleteMany({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
in: removedFields.map((field) => field.id),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return persistedFields;
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
export interface GetRecipientsForDocumentOptions {
|
|
||||||
documentId: number;
|
|
||||||
userId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getRecipientsForDocument = async ({
|
|
||||||
documentId,
|
|
||||||
userId,
|
|
||||||
}: GetRecipientsForDocumentOptions) => {
|
|
||||||
const recipients = await prisma.recipient.findMany({
|
|
||||||
where: {
|
|
||||||
documentId,
|
|
||||||
Document: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return recipients;
|
|
||||||
};
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import { nanoid } from 'nanoid';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
export interface SetRecipientsForDocumentOptions {
|
|
||||||
userId: number;
|
|
||||||
documentId: number;
|
|
||||||
recipients: {
|
|
||||||
id?: number | null;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setRecipientsForDocument = async ({
|
|
||||||
userId,
|
|
||||||
documentId,
|
|
||||||
recipients,
|
|
||||||
}: SetRecipientsForDocumentOptions) => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new Error('Document not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingRecipients = await prisma.recipient.findMany({
|
|
||||||
where: {
|
|
||||||
documentId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const removedRecipients = existingRecipients.filter(
|
|
||||||
(existingRecipient) =>
|
|
||||||
!recipients.find(
|
|
||||||
(recipient) =>
|
|
||||||
recipient.id === existingRecipient.id || recipient.email === existingRecipient.email,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const linkedRecipients = recipients.map((recipient) => {
|
|
||||||
const existing = existingRecipients.find(
|
|
||||||
(existingRecipient) =>
|
|
||||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...recipient,
|
|
||||||
...existing,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const recipient of linkedRecipients) {
|
|
||||||
if (
|
|
||||||
recipient.sendStatus === SendStatus.SENT ||
|
|
||||||
recipient.signingStatus === SigningStatus.SIGNED
|
|
||||||
) {
|
|
||||||
throw new Error('Cannot modify recipients after sending');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistedRecipients = await prisma.$transaction(
|
|
||||||
linkedRecipients.map((recipient) =>
|
|
||||||
recipient.id
|
|
||||||
? prisma.recipient.update({
|
|
||||||
where: {
|
|
||||||
id: recipient.id,
|
|
||||||
documentId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name: recipient.name,
|
|
||||||
email: recipient.email,
|
|
||||||
documentId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: prisma.recipient.create({
|
|
||||||
data: {
|
|
||||||
name: recipient.name,
|
|
||||||
email: recipient.email,
|
|
||||||
token: nanoid(),
|
|
||||||
documentId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (removedRecipients.length > 0) {
|
|
||||||
await prisma.recipient.deleteMany({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
in: removedRecipients.map((recipient) => recipient.id),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return persistedRecipients;
|
|
||||||
};
|
|
||||||
23
packages/lib/types/next-auth.d.ts
vendored
Normal file
23
packages/lib/types/next-auth.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { User as PrismaUser } from '@prisma/client';
|
||||||
|
import type { DefaultUser } from 'next-auth';
|
||||||
|
|
||||||
|
declare module 'next-auth' {
|
||||||
|
interface Session {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User extends Omit<DefaultUser, 'id' | 'image'> {
|
||||||
|
id: PrismaUser['id'];
|
||||||
|
email?: PrismaUser['email'];
|
||||||
|
name?: PrismaUser['name'];
|
||||||
|
emailVerified?: PrismaUser['emailVerified'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'next-auth/jwt' {
|
||||||
|
interface JWT {
|
||||||
|
id: string | number;
|
||||||
|
name?: string | null;
|
||||||
|
email: string | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- AlterEnum
|
|
||||||
ALTER TYPE "FieldType" ADD VALUE 'NAME';
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- AlterEnum
|
|
||||||
ALTER TYPE "FieldType" ADD VALUE 'EMAIL';
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Field" ADD COLUMN "height" INTEGER NOT NULL DEFAULT -1,
|
|
||||||
ADD COLUMN "width" INTEGER NOT NULL DEFAULT -1;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- A unique constraint covering the columns `[documentId,email]` on the table `Recipient` will be added. If there are existing duplicate values, this will fail.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "Recipient_documentId_email_key" ON "Recipient"("documentId", "email");
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Field" ALTER COLUMN "positionX" SET DEFAULT 0,
|
|
||||||
ALTER COLUMN "positionX" SET DATA TYPE DECIMAL(65,30),
|
|
||||||
ALTER COLUMN "positionY" SET DEFAULT 0,
|
|
||||||
ALTER COLUMN "positionY" SET DATA TYPE DECIMAL(65,30),
|
|
||||||
ALTER COLUMN "height" SET DEFAULT -1,
|
|
||||||
ALTER COLUMN "height" SET DATA TYPE DECIMAL(65,30),
|
|
||||||
ALTER COLUMN "width" SET DEFAULT -1,
|
|
||||||
ALTER COLUMN "width" SET DATA TYPE DECIMAL(65,30);
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
previewFeatures = ["extendedWhereUnique"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
@@ -124,15 +123,11 @@ model Recipient {
|
|||||||
Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||||
Field Field[]
|
Field Field[]
|
||||||
Signature Signature[]
|
Signature Signature[]
|
||||||
|
|
||||||
@@unique([documentId, email])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FieldType {
|
enum FieldType {
|
||||||
SIGNATURE
|
SIGNATURE
|
||||||
FREE_SIGNATURE
|
FREE_SIGNATURE
|
||||||
NAME
|
|
||||||
EMAIL
|
|
||||||
DATE
|
DATE
|
||||||
TEXT
|
TEXT
|
||||||
}
|
}
|
||||||
@@ -143,10 +138,8 @@ model Field {
|
|||||||
recipientId Int?
|
recipientId Int?
|
||||||
type FieldType
|
type FieldType
|
||||||
page Int
|
page Int
|
||||||
positionX Decimal @default(0)
|
positionX Int @default(0)
|
||||||
positionY Decimal @default(0)
|
positionY Int @default(0)
|
||||||
width Decimal @default(-1)
|
|
||||||
height Decimal @default(-1)
|
|
||||||
customText String
|
customText String
|
||||||
inserted Boolean
|
inserted Boolean
|
||||||
Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
|
||||||
"@documenso/prisma": "*",
|
|
||||||
"@tanstack/react-query": "^4.29.5",
|
"@tanstack/react-query": "^4.29.5",
|
||||||
"@trpc/client": "^10.25.1",
|
"@trpc/client": "^10.25.1",
|
||||||
"@trpc/next": "^10.25.1",
|
"@trpc/next": "^10.25.1",
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
|
||||||
|
|
||||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
|
||||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
|
||||||
|
|
||||||
import { authenticatedProcedure, router } from '../trpc';
|
|
||||||
import {
|
|
||||||
ZSetFieldsForDocumentMutationSchema,
|
|
||||||
ZSetRecipientsForDocumentMutationSchema,
|
|
||||||
} from './schema';
|
|
||||||
|
|
||||||
export const documentRouter = router({
|
|
||||||
setRecipientsForDocument: authenticatedProcedure
|
|
||||||
.input(ZSetRecipientsForDocumentMutationSchema)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
try {
|
|
||||||
const { documentId, recipients } = input;
|
|
||||||
|
|
||||||
return await setRecipientsForDocument({
|
|
||||||
userId: ctx.user.id,
|
|
||||||
documentId,
|
|
||||||
recipients,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message:
|
|
||||||
'We were unable to set the recipients for this document. Please try again later.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
setFieldsForDocument: authenticatedProcedure
|
|
||||||
.input(ZSetFieldsForDocumentMutationSchema)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
try {
|
|
||||||
const { documentId, fields } = input;
|
|
||||||
|
|
||||||
return await setFieldsForDocument({
|
|
||||||
userId: ctx.user.id,
|
|
||||||
documentId,
|
|
||||||
fields,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message: 'We were unable to set the fields for this document. Please try again later.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
export const ZSetRecipientsForDocumentMutationSchema = z.object({
|
|
||||||
documentId: z.number(),
|
|
||||||
recipients: z.array(
|
|
||||||
z.object({
|
|
||||||
id: z.number().nullish(),
|
|
||||||
email: z.string().min(1).email(),
|
|
||||||
name: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TSetRecipientsForDocumentMutationSchema = z.infer<
|
|
||||||
typeof ZSetRecipientsForDocumentMutationSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const ZSetFieldsForDocumentMutationSchema = z.object({
|
|
||||||
documentId: z.number(),
|
|
||||||
fields: z.array(
|
|
||||||
z.object({
|
|
||||||
id: z.number().nullish(),
|
|
||||||
type: z.nativeEnum(FieldType),
|
|
||||||
signerEmail: z.string().min(1),
|
|
||||||
pageNumber: z.number().min(1),
|
|
||||||
pageX: z.number().min(0),
|
|
||||||
pageY: z.number().min(0),
|
|
||||||
pageWidth: z.number().min(0),
|
|
||||||
pageHeight: z.number().min(0),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TSetFieldsForDocumentMutationSchema = z.infer<
|
|
||||||
typeof ZSetFieldsForDocumentMutationSchema
|
|
||||||
>;
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { authRouter } from './auth-router/router';
|
import { authRouter } from './auth-router/router';
|
||||||
import { documentRouter } from './document-router/router';
|
|
||||||
import { profileRouter } from './profile-router/router';
|
import { profileRouter } from './profile-router/router';
|
||||||
import { procedure, router } from './trpc';
|
import { procedure, router } from './trpc';
|
||||||
|
|
||||||
@@ -7,7 +6,6 @@ export const appRouter = router({
|
|||||||
hello: procedure.query(() => 'Hello, world!'),
|
hello: procedure.query(() => 'Hello, world!'),
|
||||||
auth: authRouter,
|
auth: authRouter,
|
||||||
profile: profileRouter,
|
profile: profileRouter,
|
||||||
document: documentRouter,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|||||||
@@ -10,10 +10,22 @@ export type CardProps = React.HTMLAttributes<HTMLDivElement> & {
|
|||||||
spotlight?: boolean;
|
spotlight?: boolean;
|
||||||
gradient?: boolean;
|
gradient?: boolean;
|
||||||
degrees?: number;
|
degrees?: number;
|
||||||
|
lightMode?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||||
({ className, children, gradient = false, spotlight = false, degrees = 120, ...props }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
gradient = false,
|
||||||
|
spotlight = false,
|
||||||
|
degrees = 120,
|
||||||
|
lightMode = true,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
const mouseX = useMotionValue(0);
|
const mouseX = useMotionValue(0);
|
||||||
const mouseY = useMotionValue(0);
|
const mouseY = useMotionValue(0);
|
||||||
|
|
||||||
@@ -34,15 +46,12 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-background text-foreground group relative rounded-lg border-2 backdrop-blur-[2px]',
|
'bg-background text-foreground dark:hover:border-documenso group relative rounded-lg border-2 backdrop-blur-[2px]',
|
||||||
{
|
{
|
||||||
'gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.documenso.DEFAULT/50%)_5%,theme(colors.border/80%)_30%)]':
|
'gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.documenso.DEFAULT/50%)_5%,theme(colors.border/80%)_30%)]':
|
||||||
gradient,
|
gradient && lightMode,
|
||||||
'dark:gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.documenso.DEFAULT/70%)_5%,theme(colors.border/80%)_30%)]':
|
|
||||||
gradient,
|
|
||||||
'shadow-[0_0_0_4px_theme(colors.gray.100/70%),0_0_0_1px_theme(colors.gray.100/70%),0_0_0_0.5px_theme(colors.primary.DEFAULT/70%)]':
|
'shadow-[0_0_0_4px_theme(colors.gray.100/70%),0_0_0_1px_theme(colors.gray.100/70%),0_0_0_0.5px_theme(colors.primary.DEFAULT/70%)]':
|
||||||
true,
|
lightMode,
|
||||||
'dark:shadow-[0]': true,
|
|
||||||
},
|
},
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
"NEXT_PUBLIC_SITE_URL",
|
"NEXT_PUBLIC_SITE_URL",
|
||||||
"NEXT_PRIVATE_DATABASE_URL",
|
"NEXT_PRIVATE_DATABASE_URL",
|
||||||
"NEXT_PRIVATE_NEXT_AUTH_SECRET",
|
"NEXT_PRIVATE_NEXT_AUTH_SECRET",
|
||||||
"NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED"
|
"NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED",
|
||||||
|
"NEXT_PRIVATE_GOOGLE_CLIENT_ID",
|
||||||
|
"NEXT_PRIVATE_GOOGLE_CLIENT_SECRET"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user