Merge branch 'main' into feat/DOC-170-add-name-field

This commit is contained in:
Timur Ercan
2023-05-19 20:07:08 +02:00
committed by GitHub
24 changed files with 1322 additions and 5077 deletions

View File

@ -71,7 +71,7 @@ The current project goal is to <b>[release a production ready version](https://g
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
## Tools
# Tech
@ -86,7 +86,7 @@ Documenso is built using awesome open source tech including:
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
- Check out /packages.json and /apps/web/package.json for more
- Check out `/package.json` and `/apps/web/package.json` for more
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
# Getting Started
@ -111,7 +111,7 @@ Want to get up and running quickly? Follow these steps:
git clone https://github.com/documenso/documenso
```
- Set up your .env file using the recommendations in the .env.example file.
- Set up your `.env` file using the recommendations in the `.env.example` file.
- Run `npm run dx` in the root directory
- This will spin up a postgres database and inbucket mail server in docker containers.
- Run `npm run dev` in the root directory
@ -124,7 +124,7 @@ That's it! You should now be able to access the app at http://localhost:3000
Incoming mail will be available at http://localhost:9000
Your database will also be available on port `5432`. You can connect to it using your favorite database client.
Your database will also be available on port `54320`. You can connect to it using your favorite database client.
## Developer Setup
@ -150,24 +150,24 @@ Follow these steps to setup documenso on you local machine:
---
- Optional: Seed the database using <code>npm run db-seed</code> to create a test user and document
- Optional: Upload and sign <code>apps\web\ressources\example.pdf</code> manually to test your setup
- Optional: Upload and sign <code>apps/web/ressources/example.pdf</code> manually to test your setup
- Optional: Create your own signing certificate
- A demo certificate is provided in /app/web/ressources/certificate.p12
- To generate your own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signing certificate**.
- A demo certificate is provided in `/app/web/ressources/certificate.p12`
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**.
## Updating
- If you pull the newest version from main, using <code>git pull</code>, it may be necessary to regenerate your database client
- You can do this by running the generate command in /packages/prisma:
- You can do this by running the generate command in `/packages/prisma`:
```sh
npx prisma generate
```
- This is not necessary on first clone
- This is not necessary on first clone.
# Creating your own signing certificate
For the digital signature of your documents you need a signing certificate in .p12 formate (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\
<code>openssl genrsa -out private.key 2048</code>

View File

@ -1,4 +0,0 @@
{
"presets": ["next/babel"],
"plugins": []
}

View File

@ -1,3 +1,8 @@
{
"extends": ["next/babel", "next/core-web-vitals"]
}
"extends": [
"next/core-web-vitals"
],
"rules": {
"react/no-unescaped-entities": "off"
}
}

View File

@ -198,12 +198,11 @@ export default function PDFSigner(props: any) {
: props.document.User.email}{" "}
would like you to sign this document.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<p className="mt-3 text-sm md:mt-0 md:ml-6 text-right md:text-inherit">
<Button
disabled={!signingDone}
color="secondary"
icon={CheckBadgeIcon}
className="float-right"
onClick={() => {
signDocument(props.document, localSignatures, `${router.query.token}`).then(
() => {

View File

@ -5,6 +5,7 @@ import { Button, IconButton } from "@documenso/ui";
import { Dialog, Transition } from "@headlessui/react";
import { LanguageIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import SignatureCanvas from "react-signature-canvas";
import { useDebouncedValue } from "../../hooks/use-debounced-value";
const tabs = [
{ name: "Type", icon: LanguageIcon, current: true },
@ -15,6 +16,9 @@ export default function SignatureDialog(props: any) {
const [currentTab, setCurrentTab] = useState(tabs[0]);
const [typedSignature, setTypedSignature] = useState("");
const [signatureEmpty, setSignatureEmpty] = useState(true);
// This is a workaround to prevent the canvas from being rendered when the dialog is closed
// we also need the debounce to avoid rendering while transitions are occuring.
const showCanvas = useDebouncedValue<boolean>(props.open, 1);
let signCanvasRef: any | undefined;
useEffect(() => {
@ -85,7 +89,7 @@ export default function SignatureDialog(props: any) {
</div>
{isCurrentTab("Type") ? (
<div>
<div className="my-8 mb-3 border-b border-gray-300">
<div className="my-7 mb-3 border-b border-gray-300">
<input
value={typedSignature}
onChange={(e) => {
@ -98,7 +102,7 @@ export default function SignatureDialog(props: any) {
placeholder="Kindly type your name"
/>
</div>
<div className="float-right">
<div className="flex flex-row-reverse items-center gap-x-3">
<Button
color="secondary"
onClick={() => {
@ -126,47 +130,55 @@ export default function SignatureDialog(props: any) {
""
)}
{isCurrentTab("Draw") ? (
<div className="">
<SignatureCanvas
ref={(ref) => {
signCanvasRef = ref;
}}
canvasProps={{
className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
}}
clearOnResize={true}
onEnd={() => {
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
<IconButton
className="float-left block"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}></IconButton>
<div className="float-right mt-10">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}>
Cancel
</Button>
<Button
className="ml-3"
onClick={() => {
props.onClose({
type: "draw",
signatureImage: signCanvasRef.toDataURL("image/png"),
});
<div className="" key={props.open ? "closed" : "open"}>
{showCanvas && (
<SignatureCanvas
ref={(ref) => {
signCanvasRef = ref;
}}
disabled={signatureEmpty}>
Sign
</Button>
canvasProps={{
className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
}}
clearOnResize={true}
onEnd={() => {
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
)}
<div className="flex items-center justify-between">
<IconButton
className="block"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
<div className="flex flex-row-reverse items-center gap-x-3">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}>
Cancel
</Button>
<Button
className="ml-3"
onClick={() => {
props.onClose({
type: "draw",
signatureImage: signCanvasRef.toDataURL("image/png"),
});
}}
disabled={signatureEmpty}>
Sign
</Button>
</div>
</div>
</div>
) : (

View File

@ -111,7 +111,7 @@ export default function Login(props: any) {
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<a href="#" className="text-neon hover:text-neon font-medium">
<a href="#" className="text-gray-500 hover:text-neon-700 font-medium">
Forgot your password?
</a>
</div>
@ -123,7 +123,7 @@ export default function Login(props: any) {
className="group relative flex w-full">
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<LockClosedIcon
className="text-neon-dark group-hover:text-neon h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600"
className="text-neon-700 group-hover:text-neon-dark-700 h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600 duration-200"
aria-hidden="true"
/>
</span>
@ -141,7 +141,7 @@ export default function Login(props: any) {
{props.allowSignup ? (
<p className="mt-2 text-center text-sm text-gray-600">
Are you new here?{" "}
<Link href="/signup" className="text-neon hover:text-neon font-medium">
<Link href="/signup" className="text-gray-500 hover:text-neon-700 duration-200 font-medium">
Create a new Account
</Link>
</p>
@ -151,7 +151,7 @@ export default function Login(props: any) {
<Link
href="https://documenso.com"
className="text-neon hover:text-neon font-medium">
Hosted Documenso will be availible soon
Hosted Documenso will be available soon
</Link>
</p>
)}

View File

@ -187,7 +187,7 @@ export default function Signup(props: { source: string }) {
</div>
<p className="mt-2 text-center text-sm text-gray-600">
Already have an account?{" "}
<Link href="/login" className="text-neon hover:text-neon font-medium">
<Link href="/login" className="text-gray-500 hover:text-neon-700 font-medium">
Sign In
</Link>
</p>

View File

@ -0,0 +1,18 @@
import { useEffect, useState } from "react";
export function useDebouncedValue<T>(value: T, delay: number) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}

View File

@ -16,27 +16,16 @@
"@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13",
"@pdf-lib/fontkit": "^1.1.1",
"@tailwindcss/forms": "^0.5.3",
"@types/bcryptjs": "^2.4.2",
"@types/filesystem": "^0.0.32",
"@types/react-dom": "18.0.9",
"avatar-from-initials": "^1.0.3",
"base64-arraybuffer": "^1.0.2",
"bcryptjs": "^2.4.3",
"dotenv": "^16.0.3",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"file-loader": "^6.2.0",
"formidable": "^3.2.5",
"install": "^0.13.0",
"next": "13.0.3",
"next-auth": ">=4.20.1",
"next-transpile-modules": "^10.0.0",
"next": "13.2.4",
"next-auth": "^4.22.0",
"node-forge": "^1.3.1",
"node-signpdf": "^1.5.0",
"nodemailer": "^6.9.0",
"nodemailer-sendgrid": "^1.0.3",
"npm": "^9.1.3",
"pdf-lib": "^1.17.1",
"placeholder-loading": "^0.6.0",
"react": "18.2.0",
@ -46,12 +35,14 @@
"react-pdf": "^6.2.2",
"react-resizable": "^3.0.4",
"react-tooltip": "^5.7.2",
"sass": "^1.57.1",
"short-uuid": "^4.2.2",
"string-to-color": "^2.2.2",
"typescript": "4.8.4"
"string-to-color": "^2.2.2"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@types/bcryptjs": "^2.4.2",
"@types/filesystem": "^0.0.32",
"@types/react-dom": "18.0.9",
"@types/formidable": "^2.0.5",
"@types/node": "^18.11.18",
"@types/nodemailer": "^6.4.7",
@ -59,7 +50,14 @@
"@types/react-pdf": "^6.2.0",
"@types/react-resizable": "^3.0.3",
"autoprefixer": "^10.4.13",
"dotenv": "^16.0.3",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"file-loader": "^6.2.0",
"next-transpile-modules": "^10.0.0",
"postcss": "^8.4.19",
"tailwindcss": "^3.2.4"
"sass": "^1.57.1",
"tailwindcss": "^3.2.4",
"typescript": "4.8.4"
}
}
}

View File

@ -63,6 +63,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
data: {
signingStatus: SigningStatus.SIGNED,
signedAt: new Date(),
},
});
@ -86,7 +87,11 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
where: {
documentId: document.id,
type: { in: [FieldType.DATE, FieldType.TEXT] },
recipientId: { in: signedRecipients.map((r) => r.id) },
},
include: {
Recipient: true,
}
});
// Insert fields other than signatures
@ -98,7 +103,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
month: "long",
day: "numeric",
year: "numeric",
}).format(new Date())
}).format(field.Recipient?.signedAt ?? new Date())
: field.customText || "",
field.positionX,
field.positionY,

View File

@ -1,4 +1,4 @@
import { ReactElement } from "react";
import { ChangeEvent, ReactElement } from "react";
import Head from "next/head";
import Link from "next/link";
import { uploadDocument } from "@documenso/features";
@ -62,26 +62,27 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
<dl className="mt-8 grid gap-5 md:grid-cols-3 ">
{stats.map((item) => (
<Link href={item.link} key={item.name}>
<div className="overflow-hidden rounded-lg bg-white px-4 py-3 shadow sm:py-5 md:p-6">
<dt className="truncate text-sm font-medium text-gray-500 ">
<div className="overflow-hidden rounded-lg bg-white px-4 py-3 shadow hover:shadow-lg duration-300 sm:py-5 md:p-6">
<dt className="truncate text-sm font-medium text-gray-700 ">
<item.icon
className="text-neon mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-6"
className="text-neon-600 mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-6"
aria-hidden="true"></item.icon>
{item.name}
</dt>
<dd className="mt-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
<dd className="mt-3 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
{getStat(item.name, props)}
</dd>
</div>
</Link>
))}
</dl>
<div className="mt-12">
<input
id="fileUploadHelper"
type="file"
accept="application/pdf"
onChange={(event: any) => {
onChange={(event: ChangeEvent) => {
uploadDocument(event);
}}
hidden
@ -91,9 +92,10 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
onClick={() => {
document?.getElementById("fileUploadHelper")?.click();
}}
className="hover:border-neon relative block w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
className="group hover:border-neon-600 duration-200 relative block w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<svg
className="mx-auto h-12 w-12 text-gray-400"
className="mx-auto h-12 w-12 text-gray-400 group-hover:text-gray-700 duration-200"
stroke="currentColor"
fill="none"
viewBox="0 00 20 25"
@ -104,7 +106,8 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg>
<span id="add_document" className="text-neon mt-2 block text-sm font-medium">
<span id="add_document" className="text-gray-500 group-hover:text-neon-700 mt-2 block text-sm font-medium duration-200">
Add a new PDF document.
</span>
</div>

View File

@ -142,26 +142,26 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
</Button>
</div>
</div>
<div className="mt-3 mb-12">
<div className="float-right ml-3 mt-7 block w-fit">
<div className="mt-3 mb-12 flex flex-row-reverse items-center gap-x-4">
<div className="pt-5 block w-fit">
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
</div>
<SelectBox
className="float-right block w-1/4"
className="block w-1/4"
label="Created"
options={createdFilter}
value={selectedCreatedFilter}
onChange={setSelectedCreatedFilter}
/>
<SelectBox
className="float-right ml-3 block w-1/4"
className="block w-1/4"
label="Status"
options={statusFilters}
value={selectedStatusFilter}
onChange={handleStatusFilterChange}
/>
</div>
<div className="mt-20 max-w-[1100px]" hidden={!loading}>
<div className="mt-8 max-w-[1100px]" hidden={!loading}>
<div className="ph-item">
<div className="ph-col-12">
<div className="ph-picture"></div>
@ -178,7 +178,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
</div>
</div>
</div>
<div className="mt-28 flex flex-col" hidden={!documents.length || loading}>
<div className="mt-8 flex flex-col" hidden={!documents.length || loading}>
<div
className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"
hidden={!documents.length || loading}>
@ -221,13 +221,13 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{document.title || "#" + document.id}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<td className="whitespace-nowrap inline-flex py-3 gap-x-2 gap-y-1 flex-wrap max-w-[250px] text-sm text-gray-500">
{document.Recipient.map((item: any) => (
<div key={item.id}>
{item.sendStatus === "NOT_SENT" ? (
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
) : (
@ -237,8 +237,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<span id="sent_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800">
<EnvelopeIcon className="mr-1 inline h-5"></EnvelopeIcon>
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
<EnvelopeIcon className="mr-1 inline h-4"></EnvelopeIcon>
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
</span>
@ -250,9 +250,9 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<span id="read_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckIcon className="-mr-2 inline h-5"></CheckIcon>
<CheckIcon className="mr-1 inline h-5"></CheckIcon>
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
<CheckIcon className="-mr-2 inline h-4"></CheckIcon>
<CheckIcon className="mr-1 inline h-4"></CheckIcon>
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
</span>
@ -261,7 +261,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
)}
{item.signingStatus === "SIGNED" ? (
<span id="signed_icon">
<span className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
<span className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "}
{item.email}
</span>

View File

@ -4,7 +4,7 @@ import { NEXT_PUBLIC_WEBAPP_URL, classNames } from "@documenso/lib";
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
import { getDocument } from "@documenso/lib/query";
import { getUserFromToken } from "@documenso/lib/server";
import { Breadcrumb, Button, Dialog, IconButton } from "@documenso/ui";
import { Breadcrumb, Button, Dialog, IconButton, Tooltip } from "@documenso/ui";
import Layout from "../../../components/layout";
import { NextPageWithLayout } from "../../_app";
import {
@ -264,38 +264,46 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
</div>
{props.document.status !== DocumentStatus.COMPLETED && (
<div className="mr-1 flex">
<IconButton
icon={PaperAirplaneIcon}
disabled={
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
color="secondary"
className="my-auto mr-4 h-9"
onClick={() => {
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [item.id]).finally(() => {
setLoading(false);
});
<Tooltip label="Resend">
<IconButton
icon={PaperAirplaneIcon}
disabled={
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
}}>
Resend
</IconButton>
<IconButton
icon={TrashIcon}
disabled={!item.id || item.sendStatus === "SENT" || loading}
onClick={() => {
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
append(removedItem);
});
}}
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
onClick={(event: any) => {
event.preventDefault();
event.stopPropagation();
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [item.id]).finally(() => {
setLoading(false);
});
}
}}
className="mx-1 group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</Tooltip>
<Tooltip label="Delete">
<IconButton
icon={TrashIcon}
disabled={!item.id || item.sendStatus === "SENT" || loading}
onClick={(event: any) => {
event.preventDefault();
event.stopPropagation();
if (confirm("Delete this signing request?")) {
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
append(removedItem);
});
}
}}
className="mx-1 group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</Tooltip>
</div>
)}
</div>

View File

@ -16,9 +16,48 @@ module.exports = {
qwigley: ["Qwigley", "serif"],
},
colors: {
neon: "#37f095",
"neon-dark": "#2CC077",
brown: "#353434",
neon: {
DEFAULT: "#37F095",
50: "#E2FDF0",
100: "#CFFBE5",
200: "#A9F9D1",
300: "#83F6BD",
400: "#5DF3A9",
500: "#37F095",
600: "#11DE79",
700: "#0DAA5D",
800: "#097640",
900: "#054224",
950: "#032816",
},
"neon-dark": {
DEFAULT: "#2CC077",
50: "#B5EED2",
100: "#A5EAC8",
200: "#84E3B4",
300: "#62DBA0",
400: "#41D48B",
500: "#2CC077",
600: "#22925B",
700: "#17653E",
800: "#0D3722",
900: "#020906",
950: "#000000",
},
brown: {
DEFAULT: "#353434",
50: "#918F8F",
100: "#878585",
200: "#737171",
300: "#5E5C5C",
400: "#4A4848",
500: "#353434",
600: "#191818",
700: "#000000",
800: "#000000",
900: "#000000",
950: "#000000",
},
},
borderRadius: {
"4xl": "2rem",

5930
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,33 +26,31 @@
"@documenso/prisma": "*",
"@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13",
"@tailwindcss/forms": "^0.5.3",
"@types/bcryptjs": "^2.4.2",
"@types/node": "18.11.9",
"@types/react-dom": "18.0.9",
"@types/react-signature-canvas": "^1.0.2",
"avatar-from-initials": "^1.0.3",
"bcryptjs": "^2.4.3",
"dotenv": "^16.0.3",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"install": "^0.13.0",
"next": "13.0.3",
"next": "13.2.4",
"next-auth": ">=4.20.1",
"next-transpile-modules": "^10.0.0",
"npm": "^9.1.3",
"pdf-lib": "^1.17.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.41.5",
"react-hot-toast": "^2.4.0",
"react-signature-canvas": "^1.0.6",
"typescript": "4.8.4"
"react-signature-canvas": "^1.0.6"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/bcryptjs": "^2.4.2",
"@types/node": "18.11.9",
"@types/react-dom": "18.0.9",
"@types/react-signature-canvas": "^1.0.2",
"dotenv": "^16.0.3",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"next-transpile-modules": "^10.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.5"
"prettier-plugin-tailwindcss": "^0.2.5",
"typescript": "4.8.4"
}
}
}

View File

@ -1,9 +1,10 @@
import router from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
import toast from "react-hot-toast";
import { ChangeEvent } from "react";
export const uploadDocument = async (event: any) => {
if (event.target.files && event.target.files[0]) {
export const uploadDocument = async (event: ChangeEvent) => {
if (event.target instanceof HTMLInputElement && event.target?.files && event.target.files[0]) {
const body = new FormData();
const document = event.target.files[0];
const fileName: string = event.target.files[0].name;
@ -12,8 +13,10 @@ export const uploadDocument = async (event: any) => {
toast.error("Non-PDF documents are not supported yet.");
return;
}
body.append("document", document || "");
const response: any = await toast
await toast
.promise(
fetch("/api/documents", {
method: "POST",

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Recipient" ADD COLUMN "signedAt" TIMESTAMP(3);

View File

@ -3,20 +3,20 @@
"version": "0.0.0",
"private": true,
"main": "index.ts",
"devDependencies": {
"@types/node": "^18.11.18",
"ts-node": "^10.9.1",
"typescript": "4.8.4"
"scripts": {
"db-studio": "prisma studio",
"db-seed": "prisma db seed"
},
"dependencies": {
"@prisma/client": "^4.8.1",
"prisma": "^4.8.1"
},
"scripts": {
"db-studio": "prisma studio",
"db-seed": "prisma db seed"
"devDependencies": {
"@types/node": "^18.11.18",
"ts-node": "^10.9.1",
"typescript": "4.8.4"
},
"prisma": {
"seed": "ts-node --transpile-only ./seed.ts"
}
}
}

View File

@ -92,6 +92,7 @@ model Recipient {
name String @default("") @db.VarChar(255)
token String
expired DateTime?
signedAt DateTime?
readStatus ReadStatus @default(NOT_OPENED)
signingStatus SigningStatus @default(NOT_SIGNED)
sendStatus SendStatus @default(NOT_SENT)

View File

@ -6,8 +6,8 @@ export function Button(props: any) {
const isLink = typeof props.href !== "undefined" && !props.disabled;
const { color = "primary", icon, disabled, onClick } = props;
const baseStyles =
"inline-flex items-center justify-center min-w-[80px] rounded-md border border-transparent px-4 py-2 text-sm font-medium shadow-sm disabled:bg-gray-300";
const primaryStyles = "text-white bg-neon hover:bg-neon-dark";
"inline-flex gap-x-2 items-center justify-center min-w-[80px] rounded-md border border-transparent px-4 py-2 text-sm font-medium shadow-sm disabled:bg-gray-300 duration-200";
const primaryStyles = "text-gray-900 bg-neon hover:bg-neon-dark";
const secondaryStyles = "border-gray-300 bg-white text-gray-700 hover:bg-gray-50";
return isLink ? (
@ -21,7 +21,7 @@ export function Button(props: any) {
)}
hidden={props.hidden}>
{props.icon ? (
<props.icon className="mr-1 inline h-5 text-inherit" aria-hidden="true"></props.icon>
<props.icon className="inline-block h-5 text-inherit" aria-hidden="true"></props.icon>
) : (
""
)}
@ -40,7 +40,7 @@ export function Button(props: any) {
disabled={props.disabled || props.loading}
hidden={props.hidden}>
{props.icon ? (
<props.icon className="mr-1 inline h-5 text-inherit" aria-hidden="true"></props.icon>
<props.icon className="inline h-5 text-inherit" aria-hidden="true"></props.icon>
) : (
""
)}

View File

@ -0,0 +1,34 @@
import React, { useState } from "react";
import { classNames } from "@documenso/lib";
export function Tooltip(props: any) {
let timeout: NodeJS.Timeout;
const [active, setActive] = useState(false);
const showTip = () => {
timeout = setTimeout(() => {
setActive(true);
}, props.delay || 40);
};
const hideTip = () => {
clearInterval(timeout);
setActive(false);
};
return (
<div className="relative" onPointerEnter={showTip} onPointerLeave={hideTip}>
{props.children}
<div
className={classNames(
"absolute left-1/4 -translate-x-1/2 transform px-4 transition-all delay-50 duration-120",
active && "bottom-9 opacity-100",
!active && "pointer-events-none bottom-6 opacity-0"
)}>
<span className="text-neon-800 bg-neon-200 inline-block rounded py-1 px-2 text-xs">
{props.label}
</span>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export { Tooltip } from "./Tooltip";

View File

@ -2,3 +2,4 @@ export { Button, IconButton } from "./components/button/index";
export { SelectBox } from "./components/selectBox/index";
export { Breadcrumb } from "./components/breadcrumb/index";
export { Dialog } from "./components/dialog/index";
export { Tooltip } from "./components/tooltip/index";