Compare commits

...

293 Commits

Author SHA1 Message Date
3af26291c9 minor changes 2023-07-08 08:56:34 +00:00
d1bc948f3c clean up console.log() used for testing 2023-06-13 02:00:45 -04:00
2b84636993 feat: google auth without schema change 2023-06-13 01:53:12 -04:00
05238f096b feat: dark mode & theme switching
feat: dark mode & theme switching
2023-06-12 16:01:02 +10:00
dd83d4607c fix: dark mode on signup and signin pages 2023-06-11 12:26:47 -04:00
07d13c74f5 fix: signature pad in dark mode 2023-06-11 11:36:13 -04:00
64d1d6df37 resolving eslint build errors 2023-06-11 02:21:13 -04:00
877a579533 adding dark mode to feat/refresh 2023-06-11 01:50:19 -04:00
b0e364acf4 wip: create document workflow 2023-06-10 22:33:12 +10:00
803ebccee3 wip: refresh design 2023-06-09 18:22:21 +10:00
5ec97657c1 Merge pull request #199 from documenso/fix/new-slack-link
fix: expired slack link
2023-06-07 20:15:13 +10:00
02f9c38e1e Replace slack link with documen.so/slack 2023-06-07 09:59:40 +00:00
aa651fb4e0 Merge pull request #191 from eltociear/patch-1
Fix typo in pdf-editor.tsx
2023-06-06 19:01:31 +10:00
9ff8527336 fix: expired slack link 2023-06-05 21:49:39 +00:00
4e65ff3a47 Merge pull request #195 from PeerRich/patch-1
chore: fix readme Product Hunt Badges
2023-06-05 21:47:39 +10:00
effe781ce7 chore: fix readme Product Hunt Badges
Product Hunt is over, its probably better to move it into its own section.

also added product of the day!
2023-06-05 12:33:08 +01:00
a1bb360b6f Fix typo in pdf-editor.tsx
postion -> position
2023-06-03 21:58:14 +09:00
11c1b6841f Merge pull request #185 from ahiho/fix/ipv6
docs: update troubleshooting for IPv6
2023-06-03 00:44:04 +10:00
c41007e026 Revert "fix: support ipv6 for nextjs"
This reverts commit f9de6671e0aa29e25e872a80aa334d3319e3e522.
2023-06-02 18:04:52 +07:00
db99bf3674 Revert "fix: custom nextjs server"
This reverts commit 8f9a5f4ec7d834970a3e2b0778ce94218c997a8f.
2023-06-02 18:04:52 +07:00
3caa01ab53 Revert "fix: add custom nextjs server to docker"
This reverts commit 5dbe7b26286234db542921d9ded000c522c9a31e.
2023-06-02 18:04:52 +07:00
20b618c70f docs: update troubleshooting for IPv6 2023-06-02 18:04:52 +07:00
bbedd6d3de fix: add custom nextjs server to docker 2023-06-02 18:04:52 +07:00
054480500f fix: custom nextjs server 2023-06-02 18:04:52 +07:00
15b5f31a74 fix: support ipv6 for nextjs 2023-06-02 18:04:52 +07:00
316fb49339 fix: disable subscriptions in example env 2023-06-02 19:03:59 +10:00
fc1b3be5ad Merge pull request #184 from The-Robin-Hood/bugfix/docker_script_update
docker script updated 🐳
2023-06-02 18:26:21 +10:00
20b51198b4 docker script updated 🐳 2023-06-01 22:24:58 +05:30
f80edf3f94 Merge pull request #181 from ahiho/fix/docker-image-typo
typo: documentso >> documenso
2023-06-02 00:06:28 +10:00
08faabc813 Merge pull request #182 from JonasPardon/patch-1
Fix typos in example env
2023-06-02 00:04:50 +10:00
0a7ed0701c fix: add turbo entries for other platforms to package-lock
Package managers such as NPM behave strangely when adding
packages such as swc and turborepo which contain platform
variants.

During a first time install they will include only the current
devices platform while a clean node_modules and package-lock
will result in all platforms being included.

This change adds those missed platforms by performing the above step and porting it back to our existing package-lock.
2023-06-01 23:25:49 +10:00
488cf58f0e Fix typos in example env
Just noticed some typos while setting up a local copy and thought I'd fix them up real quick.
2023-06-01 10:04:26 +02:00
dd4568b7fa typo: documentso >> documenso 2023-06-01 13:58:18 +07:00
893ab9bea5 Merge pull request #167 from dephraiim/fix/dashboard-logo
fix: dashboard logo
2023-05-30 23:29:57 +10:00
2aaeab3217 Merge pull request #176 from The-Robin-Hood/bugfix/docker_compose
Package.json Docker script fix
2023-05-30 22:50:57 +10:00
0a5de18235 minor script fix 2023-05-30 15:43:58 +05:30
b5e03359c1 fix: mark truncateTitle as optional 2023-05-30 20:11:23 +10:00
a266e4f9d4 Merge pull request #150 from The-Robin-Hood/bugfix/long_filename
Long filename fix 🗄
2023-05-30 20:04:19 +10:00
eccd9b5cd3 Merge branch 'main' into bugfix/long_filename 2023-05-30 20:03:19 +10:00
fdbcf33210 Merge pull request #164 from doug-andrade/next
Simplified next.config.js and removed next-transpile-modules dependency.
2023-05-30 20:02:17 +10:00
6048555e4a Merge branch 'main' into next 2023-05-30 19:55:31 +10:00
e33f31c483 Merge pull request #165 from doug-andrade/fix-typos
typo: /ressources to /resources
2023-05-30 19:54:32 +10:00
fe82e3c84f Merge pull request #171 from leerob/turbo
Add turborepo to monorepo.
2023-05-30 19:34:53 +10:00
7684a49b7d Merge branch 'main' into bugfix/long_filename 2023-05-30 14:30:55 +05:30
d8ad4b4b2b fix: add turbo dep and start command 2023-05-30 18:56:41 +10:00
e40ebd84d4 Merge pull request #174 from doug-andrade/fonts
loading fonts with next/font for no layout shift
2023-05-30 18:48:12 +10:00
a340b9c481 Merge pull request #173 from piyushkrmaurya/prisma_deprecations
fix: deprecated type checks and imports from @prisma/client
2023-05-30 18:35:32 +10:00
307b0cc9d9 fixed height on login/signup pages 2023-05-30 02:36:43 -04:00
3e94491474 fixed next/font load on ALL pages and toast. 2023-05-30 02:17:34 -04:00
007fe44db8 loading fonts with next/font 2023-05-29 22:25:36 -04:00
1e6f65f92d Explicit deps 2023-05-29 19:46:24 -05:00
82fbedf8e3 fix: deprecated type checks and imports from @prisma 2023-05-30 00:24:18 +05:30
2f3be1cfe5 Add turborepo to monorepo. 2023-05-29 10:38:24 -05:00
8ecd5cf215 fix: respect truncate title prop 2023-05-29 18:47:54 +10:00
f5091dd4d7 Merge pull request #166 from doug-andrade/tailwind
update: add new brand color as palette in tailwind config
2023-05-29 18:40:09 +10:00
4c06b5e640 fix: logo only on dashboard 2023-05-29 08:01:12 +00:00
b477799d70 update: add new brand color as palette in tailwind config 2023-05-28 23:17:52 -04:00
b928993510 typo: /ressources >> /resources 2023-05-28 20:06:43 -04:00
ad4d844b4d remove: next-transpile-modules dependency 2023-05-28 19:53:44 -04:00
3444d7fd93 task: simplify next.config.js 2023-05-28 19:44:00 -04:00
3e220135be fix: readme format 2023-05-28 17:42:54 +02:00
095c391d45 Merge pull request #163 from documenso/ElTimuro-patch-1
We are LIVE on Product Hunt 🚨 Come say hi :)
2023-05-28 10:16:35 +02:00
b0e4fa9e1d We are LIVE on Product Hunt 🚨 Come say hi :)
We are LIVE on Product Hunt 🚨 Come say hi :) 
https://www.producthunt.com/posts/documenso
2023-05-28 10:16:22 +02:00
f6bff1649b fix: disable empty state add doc without subscr. 2023-05-28 06:38:55 +02:00
b2b499f397 chore: update signup link 2023-05-28 06:16:48 +02:00
eb18a7e11c feat: update password in dashboard 2023-05-28 13:11:09 +10:00
89d9e02464 fix: update logo 2023-05-28 13:10:09 +10:00
a83b09f4db fix: update favicon 2023-05-28 13:07:47 +10:00
e445830ffb fix: convert readFile to buffer 2023-05-28 08:04:16 +10:00
bfff81dd3c fix: apply encoding on buffer level 2023-05-28 07:43:02 +10:00
02129aab73 Merge pull request #162 from documenso/feature/update-branding
Feature/update branding
2023-05-27 18:23:22 +02:00
e7386928fa Merge branch 'main' into feature/update-branding 2023-05-27 18:22:13 +02:00
7890b4adf1 feat: update logo 2023-05-27 18:18:34 +02:00
6aa40b2547 feat: update logo 2023-05-27 18:18:21 +02:00
c142c1bd54 feat: favicon logo 2023-05-27 18:11:49 +02:00
980bd0d485 fix: convert readFile to buffer 2023-05-28 00:53:43 +10:00
17d51354d7 fix: support cert file encodings 2023-05-28 00:39:07 +10:00
0881abdee4 Merge pull request #159 from documenso/feat/support-custom-cert-paths
feat: support leading cert from custom path
2023-05-27 23:39:08 +10:00
a300c3fb3a Merge branch 'main' into feat/support-custom-cert-paths 2023-05-27 23:38:33 +10:00
5e07b8bd92 Merge pull request #161 from documenso/ElTimuro-patch-1
PRODUCT HUNT LAUNCH TOMORROW
2023-05-27 12:04:55 +02:00
7b1d626f9a PRODUCT HUNT LAUNCH TOMORROW
WE LAUNCH ON PRODUCT HUNT TOMORROW.
https://dub.sh/documenso-launch
2023-05-27 12:04:44 +02:00
de46d0f4ab fix: support passphrase env var 2023-05-27 01:31:48 +10:00
cc7ab171b1 Merge pull request #160 from documenso/ElTimuro-patch-1
Update README.md
2023-05-26 17:23:16 +02:00
466941dbc2 Update README.md 2023-05-26 17:22:56 +02:00
0564792604 fix: remove unused import 2023-05-27 01:08:33 +10:00
32f904ad68 feat: support leading cert from custom path 2023-05-27 01:07:07 +10:00
748f842115 fix: update prop typo, extract truncate method 2023-05-25 18:43:37 +10:00
ecaec356a1 Product Hunt Hint 2023-05-23 17:22:40 +02:00
38f730c730 added EE folder and EE license scaffold 2023-05-23 17:09:42 +02:00
2b4a9fbe21 Merge pull request #145 from documenso/feat/add-subscriptions
Add subscriptions
2023-05-22 16:43:10 +02:00
106ac40fb1 texts, monthly billing default 2023-05-21 21:01:34 +02:00
62ac181193 Merge branch 'main' into feat/add-subscriptions 2023-05-21 20:44:07 +02:00
9580100d66 Merge pull request #153 from documenso/doc-215-link-token
fix: pass recipient token to signed page
2023-05-21 20:37:39 +02:00
38a8279757 fix 2023-05-21 20:36:38 +02:00
ed77000746 fix: pass recipient token to signed page 2023-05-21 20:28:06 +02:00
73b72c6cce fix NEXT_PUBLIC_ALLOW_SIGNUP reference 2023-05-21 20:10:06 +02:00
b2aa4d6587 Merge branch 'main' into feat/add-subscriptions 2023-05-21 19:08:23 +02:00
bde80bf2c9 clean up debug 2023-05-21 18:52:35 +02:00
1e505088ad fix: hide billing if the feature flag hasn't been passed 2023-05-21 23:00:54 +10:00
ae0799168a clean up submodule 2023-05-19 20:02:39 +02:00
b5ec3cc817 Merge branch 'main' into feat/add-subscriptions 2023-05-19 19:47:23 +02:00
4a2162478e Merge branch 'main' into bugfix/long_filename 2023-05-19 19:33:22 +02:00
370f38457b Merge pull request #151 from abielzulio/abielzulio-fix-typo
fix(`typo`): `availible` → `available`
2023-05-19 18:58:55 +02:00
1a5948c50e Merge branch 'bugfix/long_filename' of https://github.com/The-Robin-Hood/documenso into bugfix/long_filename 2023-05-15 15:43:32 +05:30
8bf6594cf4 prop name changed 📛 2023-05-15 15:43:29 +05:30
b6ff01ef86 Merge branch 'main' into bugfix/long_filename 2023-05-15 15:40:04 +05:30
9b993c08f1 Truncate prop added 2023-05-15 15:39:37 +05:30
f34813e450 Update login.tsx 2023-05-14 20:13:45 +07:00
8f6c6dccf4 Merge pull request #148 from piyushkrmaurya/main
Add tooltip to recipients action buttons
2023-05-12 07:42:24 +10:00
826704c21f Adds animation to tooltip 2023-05-11 21:00:05 +05:30
4f47bbb552 Adds consistent 'className' to action buttons 2023-05-11 14:04:40 +05:30
825231fe2a Adds tooltip to recipient action buttons 2023-05-11 12:15:41 +05:30
051e681701 Long filename fix 🗄 2023-05-10 20:13:07 +05:30
012d2a9a09 Merge pull request #147 from dephraiim/readme-fixes
Readme Fixes
2023-05-09 17:28:27 +10:00
85c593d8e3 fix typo 2023-05-09 07:24:38 +00:00
0f28692a39 Update readme 2023-05-07 11:25:12 +00:00
22bc854cac feat: add warnings for subscription lapses and cancellations 2023-05-06 16:40:37 +10:00
d2c5657093 fix: update signup env var 2023-05-06 16:11:56 +10:00
6934e573d5 feat: add guards and subscription ui 2023-05-06 16:08:21 +10:00
7eaa00b836 feat: add stripe api handlers 2023-05-06 14:34:43 +10:00
e7e881be01 fix: update env types 2023-05-06 14:34:20 +10:00
e80997f462 fix: update env vars 2023-05-06 14:33:42 +10:00
da0166b746 fix: tidy stripe feature and add provider 2023-05-06 14:33:27 +10:00
900b816ae0 feat: stripe handlers and fetchers 2023-05-05 20:08:18 +10:00
ed3e4d22ef feat: scaffhold subscription table and ui 2023-05-05 19:29:42 +10:00
bf84ec8962 Merge pull request #74 from Mythie/chore/optimise-deps
chore: optimise depedency tree
2023-05-01 20:38:24 +10:00
1abfa93551 Merge branch 'main' into chore/optimise-deps 2023-05-01 20:36:37 +10:00
039cc75882 Merge pull request #139 from documenso/fix/improve-general-styling
chore: improve general styling
2023-05-01 20:05:17 +10:00
8457823d8e fix: improve sign in and sign up pages 2023-05-01 20:01:36 +10:00
d135df827a fix: improve general styling
Improve the general styling of the app by removing floats and replacing it `flex`. Additionally, improve the constrast of certain parts of the app and add some transitions to hover changes.
2023-05-01 20:01:35 +10:00
d2301a923b Merge pull request #140 from documenso/feat/DOC-210-sign-dialog-broken-on-second-opening
fix: debounce display of signing canvas
2023-04-28 19:12:10 +02:00
108614bf46 Merge branch 'main' into feat/DOC-210-sign-dialog-broken-on-second-opening 2023-04-28 18:22:57 +02:00
adf69edd54 Merge pull request #141 from documenso/fix/DOC-214-date-field-appears-for-all-recipients
fix: date field appears for all recipients
2023-04-28 18:20:52 +02:00
82139f6b2d Merge branch 'main' into fix/DOC-214-date-field-appears-for-all-recipients 2023-04-25 11:51:23 +02:00
270c82759c Merge pull request #137 from zahid47/issue-131-redirect-to-dashboard-if-logged-in
Redirect to /dashboard if auth user tries to access /login or /signup
2023-04-25 11:15:11 +10:00
01c7903efa Merge pull request #142 from raysubham/fix/keep-url-state-in-sync
feat: Keep the URL query params and UI state in sync when status filter changes
2023-04-25 10:48:30 +10:00
64b755d5ba Merge branch 'main' into fix/keep-url-state-in-sync 2023-04-25 10:48:07 +10:00
8788b64585 Merge pull request #143 from mikeriss/fix-typo
Fix: typos on Readme
2023-04-25 10:41:27 +10:00
c9547057f6 fixed addional typos
typos fixed
2023-04-24 19:59:56 +02:00
17e688c222 typo
changed machnine to machine
2023-04-24 19:51:05 +02:00
f5a42e694d Updated README.md typo
changed a typo from signging to signing
2023-04-24 19:48:34 +02:00
b2d09216c8 rename function 2023-04-24 23:13:38 +05:30
6d30a486ab added type for statusFilter 2023-04-24 19:37:41 +05:30
dc6217b14e feat(Documents Filter): Keep the URL and UI state in sync when status filter changes 2023-04-24 19:16:56 +05:30
a6171ec4f3 Merge branch 'main' into fix/DOC-214-date-field-appears-for-all-recipients 2023-04-23 10:36:17 +10:00
d0f962598c Merge branch 'main' into feat/DOC-210-sign-dialog-broken-on-second-opening 2023-04-21 15:49:40 +02:00
81fd9ff749 fix: date field appears for all recipients
Updates the signing endpoint to only apply changes to the Date field for the current signer. This is made possible through the addition of the `signedAt` column within the database.

Resolves the issue with one signer filling the date for all recipients and also ensures that the date of signing on a document won't always be todays date after each recipient has signed.
2023-04-21 23:43:54 +10:00
4dcb0a684d fix: debounce display of signing canvas
Debounces the display of the signing canvas to avoid situtations where the canvas renders to 2px due to rendering while a transition is being performed.
2023-04-21 23:18:36 +10:00
ab96990d43 render PR env debug 2023-04-21 13:29:51 +02:00
ad5b2bcf82 fix: pr env condition 2023-04-21 12:59:53 +02:00
6f18be6b5b add render preview env support 2023-04-21 12:42:31 +02:00
8039871ab1 Merge pull request #130 from Mythie/fix/can-add-signature-space-for-empty-recipients
fix: disable selection for draft recipients
2023-04-20 17:26:01 +02:00
4b9840d7e0 Merge branch 'main' into fix/can-add-signature-space-for-empty-recipients 2023-04-20 17:25:39 +02:00
544a16caff Merge pull request #135 from Mythie/fix/signing-email-breaks-on-small-decices
fix: signing email breaks on small devices
2023-04-20 17:19:21 +02:00
989d036e54 Merge branch 'main' into fix/signing-email-breaks-on-small-decices 2023-04-20 17:14:00 +02:00
894f8720b8 Merge pull request #134 from SauravL3010/bugfix-#71/invalid-email-hint
Toast error for invalid email
2023-04-19 23:58:13 +10:00
70ea3ceaf3 fix: improve types 2023-04-19 23:56:39 +10:00
80d26adf9c add toast error for invalid email 2023-04-19 23:56:39 +10:00
b4e21f97e3 Merge pull request #133 from dephraiim/docker-container-name
Use `documenso` as container name for local development
2023-04-19 23:32:00 +10:00
95c3be9a77 chore: optimise depedency tree 2023-04-19 23:30:14 +10:00
52f554a636 Merge pull request #136 from dephraiim/doc-223
Remove Input Placeholders
2023-04-19 22:55:26 +10:00
849885b5b3 fix: redirect to /dashboard if auth user tries to access /login or /signup 2023-04-19 13:11:02 +06:00
bcc2530484 Declutter Textarea by removing placeholders 2023-04-16 23:45:57 +00:00
d863f89232 fix: signing email breaks on small devices
Currently the signing email displays poorly on small devices with the line wrapping
causing the button to look broken.

Resolve this by using whitespace no-wrap.
2023-04-17 07:01:41 +10:00
84e3d29589 Use documenso as container name for local development 2023-04-16 18:29:40 +00:00
ba3ffe68ea fix: disable selection for draft recipients 2023-04-16 23:02:50 +10:00
5c58b32d92 Merge branch 'main' of https://github.com/documenso/documenso 2023-04-15 20:35:36 +02:00
f10bafd998 cleanup 2023-04-15 20:35:33 +02:00
2cf8896e46 Merge branch 'main' of https://github.com/documenso/documenso 2023-04-15 20:33:38 +02:00
e873af3ec9 cleanup 2023-04-15 20:31:38 +02:00
06501bde60 cleanup 2023-04-15 20:31:24 +02:00
0dcab27e65 fix: openshift build does not allow private repos 2023-04-15 20:26:35 +02:00
ff2334ab55 fix: openshift build does not allow private repos
https://github.com/documenso/documenso/issues/79
2023-04-15 20:18:30 +02:00
63bd044723 feat: npm run d for ultra quick start 2023-04-15 20:04:28 +02:00
b111874d7c fix: redirect users sessions not found in databae 2023-04-15 19:54:04 +02:00
21149f82ba Merge pull request #61 from Mythie/feat/docker-environment
feat: add docker support and docker-compose quickstart
2023-04-15 19:44:33 +02:00
cb77a40fd9 fix: update postgres port 2023-04-13 23:43:42 +10:00
7aa7485388 fix: migrate dx.sh to package scripts 2023-04-13 22:52:54 +10:00
984084dd3b Merge branch 'main' into feat/docker-environment 2023-04-13 14:50:36 +02:00
421327432a added migration for doc-208 (allow document delete with sigantures) 2023-04-11 16:19:18 +02:00
134e366c27 Merge pull request #45 from SauravL3010/fix-#41-db-migration-Signature_recipientId_fkey
Fix-#41: Change referential action for Signature_recipientId_fkey
2023-04-11 15:50:22 +02:00
c79592cd0a Merge branch 'main' into fix-#41-db-migration-Signature_recipientId_fkey 2023-04-11 15:34:32 +02:00
f7cc44f138 Merge pull request #63 from dephraiim/doc-205
Disable the edit and add signer button for completed documents
2023-04-11 15:33:25 +02:00
60ff4fc992 Merge pull request #64 from dephraiim/doc-213
Send email notification to signers on document signing completion
2023-04-11 15:12:54 +02:00
e4e44b7f22 Replace fragment with null 2023-04-10 01:34:20 +00:00
6034e7a21e Send email notification to signers on document signing completion with signed document 2023-04-09 13:15:44 +00:00
2a34cc26c6 Replace empty string with fragments 2023-04-09 12:39:18 +00:00
6ea38efd9d chore: tidy script 2023-04-09 22:36:28 +10:00
0ce66a7957 Redirect breadcrump link on completed to avoid editing 2023-04-09 12:34:26 +00:00
49cb50ed6e feat: add down flag for stopping environment 2023-04-09 22:33:14 +10:00
065efabb39 Change wording on completed signers page 2023-04-09 12:29:31 +00:00
e86d4cc719 Disable the edit and add signer button for completed documents 2023-04-09 12:26:48 +00:00
5dd3713475 feat: add docker support and docker-compose quickstart
Add support for production container builds using the provided `Dockerfile` and `build.sh` script. This can later be used with actions to automatically publish to the provided docker registry.

Additionally, support an accelerated developer quickstart using `docker-compose`. Developers can now run the `dx` npm command to quickly spin up a database and mail server.
2023-04-08 23:20:42 +10:00
30c1c76dd7 Merge pull request #44 from SauravL3010/fix-recipient-selector
small fix for recipient-selector
2023-04-07 10:52:49 +02:00
22e191e98c Merge pull request #38 from Mythie/fix/improve-text-insertion-accuracy
fix: improve text insertion accuracy
2023-04-07 10:46:23 +02:00
5db54d3b8c Cange referential action for Signature_recipientId_fkey 2023-04-06 21:26:26 -04:00
593c317bf1 small fix for recipient-selector
ListBox options must be unique
2023-04-06 14:09:08 -04:00
ee4ca018d8 fix: improve text insertion accuracy
Previous inserted text would appear a little off center from where the user had selected which could cause some frustration.

We improve upon this by updating the code responsible for centering the text to behave in a more accurate manner. From what I can tell it looks to be quite solid but could do with more rigorous testing with shorter and longer inputs.

You can see the improved accuracy in action here:

https://www.loom.com/share/1095fee7605c4790b8b30f573a04f0f0
2023-04-06 23:34:53 +10:00
e3db462587 Merge pull request #34 from dephraiim/prettier-config
[new] Add `prettier`
2023-04-06 15:03:39 +02:00
739d29d753 Merge branch 'main' into prettier-config 2023-04-06 15:02:17 +02:00
964e749039 Update prettier styling 2023-04-04 22:10:30 +00:00
84b57d715c Apply prettier config to all files 2023-04-04 22:02:32 +00:00
85f2b5e84a Add prettier config 2023-04-04 22:00:01 +00:00
51c63715d6 unused imports 2023-04-04 21:15:52 +02:00
a9ad586035 Merge pull request #32 from litaesther10/DOC-190-Dashboard-Metrics
Updated Dashboard Metrics
2023-04-04 17:54:30 +02:00
f3b68772a6 Merge branch 'main' into DOC-190-Dashboard-Metrics 2023-04-04 17:51:47 +02:00
2d0fb2879d doc-135 2023-04-04 17:46:23 +02:00
2291744580 typo 2023-04-04 14:22:25 +02:00
0c3305b11d setup STMP from .env 2023-04-04 14:20:36 +02:00
4031faec46 seed example pdf 2023-04-04 13:45:24 +02:00
a9cd5e0d94 Any PDFs hint 2023-04-04 13:19:46 +02:00
24e044097c example pdf 2023-04-04 12:31:28 +02:00
2bdfb884ec added database init script 2023-04-04 12:02:09 +02:00
ff85c294b2 empty state label consistency 2023-04-04 11:38:28 +02:00
67328b4504 Merge branch 'main' into DOC-190-Dashboard-Metrics 2023-03-28 16:06:37 +02:00
900ec32697 Updated Dashboard Metrics 2023-03-28 16:04:22 +02:00
0344ac324c Merge pull request #31 from dephraiim/doc-82
Make dialog into a component
2023-03-28 15:58:48 +02:00
b3e89b16bc Add types to Dialog component 2023-03-28 12:55:20 +00:00
a9befd342c Use dynamic values for title and icon for dialog 2023-03-28 12:47:40 +00:00
16f6da01c0 Move dialog into a seperate component 2023-03-28 12:42:00 +00:00
f8f941a9cd add inital component to @documenso/ui 2023-03-28 12:15:45 +00:00
e3059cfb34 add recipient to add fields hint 2023-03-27 13:07:35 +02:00
e79a622ddd block non pdf upload 2023-03-27 12:58:17 +02:00
9945cfb2c7 change upload to add 2023-03-27 12:53:03 +02:00
f32c3d999a background fixes firefox 2023-03-27 12:47:16 +02:00
4bb5064477 allow only pdf upload (clientside) 2023-03-26 20:07:58 +02:00
c655cb52ad Merge pull request #30 from documenso/doc-184
bugfix click on signing on mobile
2023-03-26 20:05:06 +02:00
3d0d7d1245 bugfix click on signing on mobile 2023-03-26 20:03:46 +02:00
f96bf757e2 Merge pull request #28 from litaesther10/DOC-186-Page-Margins
Added margins and borders
2023-03-23 16:22:18 +01:00
3f897abffa Merge branch 'main' into DOC-186-Page-Margins 2023-03-23 13:00:22 +01:00
df238e2be3 Merge pull request #29 from dephraiim/active-nav-bug
fix settings active bug
2023-03-23 12:59:46 +01:00
2b83e28e6d md screens margins and items center 2023-03-23 12:42:29 +01:00
6f31dacd74 fix settings active bug 2023-03-22 18:44:03 +00:00
d4324538cc Inline items, left aligned 2023-03-22 17:12:15 +01:00
2f2b708bfe Merge branch 'main' into DOC-186-Page-Margins 2023-03-22 11:13:44 +01:00
7656d4259e Added margins and borders 2023-03-22 11:10:01 +01:00
d509a6178f Merge pull request #27 from dephraiim/doc-187
Close panel on when user clicks on a nav link
2023-03-22 11:05:46 +01:00
2fed1a7034 Close nav on profile button click 2023-03-22 09:45:15 +00:00
de3c500fea Close panel on when user clicks on a nav link 2023-03-21 23:13:56 +00:00
dc0c78f270 Merge pull request #25 from dephraiim/email-null-bug
Custom message if name is defined or null in email template
2023-03-21 20:03:14 +01:00
a3e17e9f3e Custom message if name is defined or null 2023-03-21 18:44:45 +00:00
7d79e10587 Merge pull request #24 from dephraiim/settings-navbar-bug
Route to Profile on Settings
2023-03-21 19:16:07 +01:00
1a37998f39 Merge branch 'main' into settings-navbar-bug 2023-03-21 18:12:49 +00:00
2d69783ca1 Merge pull request #21 from litaesther10/DOC-185-buttons-alignment
Optimising for mobile
2023-03-21 19:11:11 +01:00
b7cc4aed9b Route to profile on settings click on navbar 2023-03-21 17:53:47 +00:00
3dfa8fc597 Fixed broken css 2023-03-21 18:11:55 +01:00
d5d3b17623 Merge branch 'main' into DOC-185-buttons-alignment 2023-03-21 15:24:43 +01:00
4710176f78 Optimising for mobile 2023-03-21 15:03:59 +01:00
f22d4ebeab Merge pull request #20 from documenso/doc-162
Doc 162
2023-03-21 14:31:04 +01:00
d32b9871db revert regression 2023-03-21 14:30:40 +01:00
0e9aa4ab62 Merge branch 'main' into doc-162 2023-03-21 14:16:47 +01:00
9e536e95b6 default allow signup 2023-03-21 09:21:57 +01:00
a57e0b2b57 coming soon hint 2023-03-20 15:45:57 +01:00
a1736afc62 Merge pull request #15 from documenso/doc-182
Doc 182
2023-03-20 15:15:02 +01:00
91b206e3d7 add token to download link to allow download without user 2023-03-20 15:12:51 +01:00
d37dd000af get document without relying on logged in user 2023-03-20 15:12:13 +01:00
7d6bd00a22 allow adding field via recipient token for signing 2023-03-20 15:11:20 +01:00
8505b9cd10 Merge pull request #14 from documenso/doc-167
Doc 167
2023-03-19 15:07:24 +01:00
dd67e1a6f0 qoc 2023-03-19 15:06:01 +01:00
9009506bb6 explicit true criteria 2023-03-19 15:05:33 +01:00
025e6a4eb1 feature flag for signup 2023-03-19 14:59:10 +01:00
72914c49c4 build fix 2023-03-19 14:20:28 +01:00
7d0c91e565 Merge pull request #13 from documenso/doc-130
Doc 130
2023-03-19 14:07:57 +01:00
36526119b2 bugfix 401 redirect 2023-03-19 14:05:32 +01:00
156b7a69e7 bugfix doc-130 document does not render because there is no user and the request is still valid 2023-03-19 14:05:20 +01:00
4eb4759381 401 redirect fix 2023-03-19 13:59:32 +01:00
1bfac711ac build fix 2023-03-19 12:43:10 +01:00
cb2d77c609 doc-171 2023-03-19 12:31:54 +01:00
253e5cfcfa show all fields while signing, date field design 2023-03-19 12:13:13 +01:00
ff16972646 hide free_signature field in editor 2023-03-19 11:54:26 +01:00
3ba6afabfc cleanup debug 2023-03-19 11:53:36 +01:00
3961402c70 comment 2023-03-19 11:48:32 +01:00
7fc228a562 Merge branch 'development' 2023-03-19 11:34:19 +01:00
899dd205f2 Merge branch 'main' of https://github.com/documenso/documenso 2023-03-19 11:34:14 +01:00
4a915134e4 Update CONTRIBUTING.md 2023-03-19 11:31:12 +01:00
266ecf0f8d bugfix racecondition in adding field to ui in parallel 2023-03-19 11:17:04 +01:00
071398273a bugfix multiple fields added after field type change: removed "drag drop" feeling handlers 2023-03-19 11:08:15 +01:00
6419d22155 Merge branch 'development' into doc-162 2023-03-19 10:52:57 +01:00
738c798dbd block editing completed documents 2023-03-19 10:52:01 +01:00
cd5f6fde32 remove debug statement 2023-03-19 10:28:15 +01:00
93654ccae5 check for already inserted fields 2023-03-19 10:23:55 +01:00
7c830d3607 bugfix don't show half created recipient 2023-03-19 10:21:19 +01:00
559432cc15 update package log 2023-03-17 19:43:25 +01:00
2400c34c71 update package lock 2023-03-17 15:37:46 +01:00
ff977c6bff nextauth security update 2023-03-17 15:35:54 +01:00
526be3b906 security update for next-auth github.com/documenso/documenso/security/dependabot/4 2023-03-17 15:31:37 +01:00
9f700ad0b2 cosmetics 2023-03-17 15:26:41 +01:00
f1dc5687d7 remove unused dep, move signature canvas types to deps 2023-03-17 15:24:37 +01:00
793902ae54 docs 2023-03-17 15:08:06 +01:00
f77f101e67 unused deps 2023-03-17 15:05:38 +01:00
49f36a103b move website to documenso org repo 2023-03-17 15:04:32 +01:00
6c7ee3edf4 Update README.md
badge fix, remove gitmoji
2023-03-17 13:43:42 +01:00
c819ed3cfb Update README.md
📑
2023-03-15 15:50:00 +01:00
433 changed files with 21894 additions and 26939 deletions

View File

@ -1,19 +1,17 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
node_modules
.pnp
.pnp.js
# testing
/coverage
coverage
# next.js
/.next/
/out/
# production
/build
.next/
out/
build
# misc
.DS_Store
@ -23,15 +21,16 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# turbo
.turbo
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

View File

@ -1,21 +1,19 @@
# Database
# You use the provided remote test database, courtesy of the documenso team: postgres://documenso_test_user:GnmLG14u12sd9zHsd4vVWwP40WneFJMo@dpg-cf2hljh4reb5o45oqpq0-a.oregon-postgres.render.com/documenso_test_e2i3
# It is however recommend, that you set up a local Postgres SQL instance
# ⚠ WARNING: The test database can be resetted or taken offline at any point
# ⚠ WARNING: Please be aware that nothing written to the test databae is private.
DATABASE_URL=''
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="secret"
# URL
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# AUTH
# For more see here: https://next-auth.js.org
NEXTAUTH_SECRET='lorem ipsum sit dolor random string for encryption this could literally be anything'
NEXTAUTH_URL='http://localhost:3000'
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
# MAIL
# Get a Sendgrid Api key here: https://signup.sendgrid.com
# You can also configure you own SMTP server using Nodemailer in sendMailts. (currently not possible via config)
SENDGRID_API_KEY=''
# Sender for signing requests and completion mails.
MAIL_FROM=''
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
NEXT_PRIVATE_STRIPE_API_KEY=
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED=false
# This is only required for the marketing site
NEXT_PRIVATE_REDIS_URL=
NEXT_PRIVATE_REDIS_TOKEN=

13
.eslintrc.cjs Normal file
View File

@ -0,0 +1,13 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['@documenso/eslint-config'],
rules: {
'@next/next/no-img-element': 'off',
},
settings: {
next: {
rootDir: ['apps/*/'],
},
},
};

30
.gitignore vendored
View File

@ -1,19 +1,17 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
node_modules
.pnp
.pnp.js
# testing
/coverage
coverage
# next.js
/.next/
/out/
# production
/build
.next/
out/
build
# misc
.DS_Store
@ -23,16 +21,16 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# turbo
.turbo
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
.env.example

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "apps/website/documenso/website"]
path = apps/website/documenso/website
url = http://github.com/eltimuro/website.git

1
.npmrc Normal file
View File

@ -0,0 +1 @@
auto-install-peers = true

21
.vscode/settings.json vendored
View File

@ -1,19 +1,10 @@
{
"files.autoSave": "afterDelay",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.formatOnType": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.removeUnusedImports": false
"source.fixAll.eslint": true
},
"typescript.tsdk": "node_modules\\typescript\\lib",
"spellright.language": ["de"],
"spellright.documentTypes": ["markdown", "latex", "plaintext"]
"eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
"javascript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.useAliasesForRenames": false,
"typescript.enablePromptUseWorkspaceTsdk": true
}

View File

@ -1,31 +1,37 @@
# Contributing to Documenso
If you plan to contribute to Documenso, please take a moment to feel awesome ✨ People like you are what open source is about ♥. Any contributions, no matter how big or small, are highly appreciated.
## Before getting started
- Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission.
- Select and issue from [here](https://github.com/documenso/documenso/issues) or create a new one
- Consider the results from the discussion in the issue
## Developing
The development branch is <code>development</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
The development branch is <code>main</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
own GitHub account and then
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch:
- Create a new branch (include the issue id and somthing readable):
```sh
git checkout -b doc-999-my-feature-or-fix
```
```sh
git checkout -b doc-999-my-feature-or-fix
```
3. See the [Developer Setup](https://github.com/documenso/documenso/blob/main/README.md#developer-setup) for more setup details.
## Building
## Building
> **Note**
> Please be sure that you can make a full production build before pushing code or creating PRs.
You can build the project with:
```bash
npm run build
```
> **Note**
> Please be sure that you can make a full production build before pushing code or creating PRs.

143
README.md
View File

@ -1,6 +1,6 @@
<p align="center" style="margin-top: 12px">
<p align="center" style="margin-top: 120px">
<a href="https://github.com/documenso/documenso.com">
<img width="250px" src="https://user-images.githubusercontent.com/1309312/224986248-5b8a5cdc-2dc1-46b9-a354-985bb6808ee0.png" alt="Documenso Logo">
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/cd7823ec-4baa-40b9-be78-4acb3b1c73cb" alt="Documenso Logo">
</a>
<h3 align="center">Open Source Signing Infrastructure</h3>
@ -11,7 +11,7 @@
<a href="https://documenso.com"><strong>Learn more »</strong></a>
<br />
<br />
<a href="https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w">Slack</a>
<a href="https://documen.so/slack">Slack</a>
·
<a href="https://documenso.com">Website</a>
·
@ -22,10 +22,10 @@
</p>
<p align="center">
<a href="https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w"><img src="https://img.shields.io/badge/Slack-documenso.slack.com-%234A154B" alt="Join Documenso on Slack"></a>
<a href="https://documen.so/slack"><img src="https://img.shields.io/badge/Slack-documenso.slack.com-%234A154B" alt="Join Documenso on Slack"></a>
<a href="https://github.com/documenso/documenso/stargazers"><img src="https://img.shields.io/github/stars/documenso/documenso" alt="Github Stars"></a>
<a href="https://github.com/documenso/documenso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
<a href="https://github.com/documenso/documensom/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
<a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
</p>
# Documenso 0.9 - Developer Preview
@ -56,30 +56,32 @@
Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure.
## Recognition
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily" alt="Documenso - The&#0032;open&#0032;source&#0032;DocuSign&#0032;alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=395047&theme=light" alt="Documenso - The&#0032;Open&#0032;Source&#0032;DocuSign&#0032;Alternative&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## Community and Next Steps 🎯
The current project goal is to <b>[release a production ready version](https://github.com/documenso/documenso/milestone/1)</b> for self-hosting as soon as possible. If you want to help making that happen you can:
- Check out the first source code release in this repository and test it
- Tell us what you think in the current [Discussions](https://github.com/documenso/documenso/discussions)
- Join the [Slack Channel](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w) for any questions and getting to know to other community members
- Join the [Slack Channel](https://documen.so/slack) for any questions and getting to know to other community members
- ⭐ the repository to help us raise awareness
- Spread the word on Twitter, that Documenso is working towards a more open signing tool
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
## Contributing
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
## Tools
- This repos uses 📝 https://gitmoji.dev/ for more expressive commit messages.
- Use 🧹 for quality of code (eg remove comments, debug output, remove unused code)
# Tech
Documenso is built using awesome open source tech including:
- [Typescript](https://www.typescriptlang.org/)
- [Javascript (when neccessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [Javascript (when necessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [NextJS (JS Fullstack Framework)](https://nextjs.org/)
- [Postgres SQL (Database)](https://www.postgresql.org/)
- [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/)
@ -87,7 +89,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
@ -97,45 +99,78 @@ Documenso is built using awesome open source tech including:
To run Documenso locally you need
- [Node.js (Version: >=18.x)](https://nodejs.org/en/download/)
- Node Package Manger NPM - included in Node.js
- Node Package Manager NPM - included in Node.js
- [PostgreSQL (local or remote)](https://www.postgresql.org/download/)
## Developer Quickstart
> **Note**: This is a quickstart for developers. It assumes that you have both [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/) installed on your machine.
Want to get up and running quickly? Follow these steps:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh
git clone https://github.com/documenso/documenso
```
- 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
- Want it even faster? Just use
```sh
npm run d
```
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 `54320`. You can connect to it using your favorite database client.
## Developer Setup
Follow these steps to setup documenso on you local machnine:
Follow these steps to setup documenso on you local machine:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh
git clone https://github.com/documenso/documenso
```
```sh
git clone https://github.com/documenso/documenso
```
- Run <code>npm i</code> in root directory
- Rename .env.example to .env
- Rename <code>.env.example</code> to <code>.env</code>
- Set DATABASE_URL value in .env file
- You can use the provided test database url (may be wiped at any point)
- Or setup a local postgres sql instance (recommened)
- Set SENDGRID_API_KEY value in .env file
- You need SendGrid account, which you can create [here](https://signup.sendgrid.com/).
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own smtp server
- Or setup a local postgres sql instance (recommended)
- Create the database scheme by running <code>db-migrate:dev</code>
- Setup your mail provider
- Set <code>SENDGRID_API_KEY</code> value in .env file
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* variables</code> in your .env
- Run <code>npm run dev</code> root directory to start
- Register a new user at http://localhost:3000/signup
---
- 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/resources/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 you own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signging certificate**.
- A demo certificate is provided in `/app/web/resources/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 neccessary to regenerate your database client
- You can do this by running the generate command in /packages/prisma:
```sh
npx prisma generate
```
- This is not neccessary on first clone
# Creating your own signging certificate
- 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`:
```sh
npx prisma generate
```
- This is not necessary on first clone.
For the digital signature of you documents you need a signign 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:
# Creating your own signing certificate
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>
@ -144,11 +179,49 @@ For the digital signature of you documents you need a signign certificate in .p1
<code>openssl req -new -x509 -key private.key -out certificate.crt -days 365</code> \
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this: \
<code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
<code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
5. Place the certificate <code>/apps/web/ressource/certificate.p12</code>
5. Place the certificate <code>/apps/web/resources/certificate.p12</code>
# Docker
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
Want to create a production ready docker image? Follow these steps:
- Run `./docker/build.sh` in the root directory.
- Publish the image to your docker registry of choice.
# Deploying - Coming Soon™
- Docker support
- One-Click-Deploy on Render.com Deploy
# Troubleshooting
## Support IPv6
In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the NextJS start command
For local docker run
```bash
docker run -it documenso:latest npm run start -- -H ::
```
For k8s or docker-compose
```yaml
containers:
- name: documenso
image: documenso:latest
imagePullPolicy: IfNotPresent
command:
- npm
args:
- run
- start
- --
- -H
- "::"
```

1
apps/marketing/README.md Normal file
View File

@ -0,0 +1 @@
# @documenso/marketing

6
apps/marketing/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const { parsed: env } = require('dotenv').config({
path: path.join(__dirname, '../../.env.local'),
});
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
transpilePackages: ['@documenso/lib', '@documenso/prisma', '@documenso/trpc', '@documenso/ui'],
env,
};
module.exports = config;

View File

@ -0,0 +1,37 @@
{
"name": "@documenso/marketing",
"version": "0.1.0",
"private": true,
"license": "AGPL-3.0",
"scripts": {
"dev": "PORT=3001 next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@documenso/lib": "*",
"@documenso/tailwind-config": "*",
"@documenso/trpc": "*",
"@documenso/ui": "*",
"@hookform/resolvers": "^3.1.0",
"framer-motion": "^10.12.8",
"lucide-react": "^0.214.0",
"micro": "^10.0.1",
"next": "13.4.1",
"next-auth": "^4.22.1",
"next-plausible": "^3.7.2",
"perfect-freehand": "^1.2.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-icons": "^4.8.0",
"typescript": "5.0.4",
"zod": "^3.21.4"
},
"devDependencies": {
"@types/node": "20.1.0",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

13
apps/marketing/process-env.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
declare namespace NodeJS {
export interface ProcessEnv {
NEXT_PUBLIC_SITE_URL?: string;
NEXT_PRIVATE_DATABASE_URL: string;
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
NEXT_PRIVATE_STRIPE_API_KEY: string;
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

View File

@ -0,0 +1,19 @@
{
"name": "Documenso",
"short_name": "Documenso",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#A2E771",
"background_color": "#FFFFFF",
"display": "standalone"
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@ -0,0 +1,41 @@
import { TClaimPlanRequestSchema, ZClaimPlanResponseSchema } from './types';
export const claimPlan = async ({
name,
email,
planId,
signatureDataUrl,
signatureText,
}: TClaimPlanRequestSchema) => {
const response = await fetch('/api/claim-plan', {
method: 'POST',
body: JSON.stringify({
name,
email,
planId,
signatureDataUrl,
signatureText,
}),
headers: {
'Content-Type': 'application/json',
},
});
const body = await response.json();
if (response.status !== 200) {
throw new Error('Failed to claim plan');
}
const safeBody = ZClaimPlanResponseSchema.safeParse(body);
if (!safeBody.success) {
throw new Error('Failed to claim plan');
}
if ('error' in safeBody.data) {
throw new Error(safeBody.data.error);
}
return safeBody.data.redirectUrl;
};

View File

@ -0,0 +1,37 @@
import { z } from 'zod';
export const ZClaimPlanRequestSchema = z
.object({
email: z
.string()
.email()
.transform((value) => value.toLowerCase()),
name: z.string(),
planId: z.string(),
})
.and(
z.union([
z.object({
signatureDataUrl: z.string().min(1),
signatureText: z.null(),
}),
z.object({
signatureDataUrl: z.null(),
signatureText: z.string().min(1),
}),
]),
);
export type TClaimPlanRequestSchema = z.infer<typeof ZClaimPlanRequestSchema>;
export const ZClaimPlanResponseSchema = z
.object({
redirectUrl: z.string(),
})
.or(
z.object({
error: z.string(),
}),
);
export type TClaimPlanResponseSchema = z.infer<typeof ZClaimPlanResponseSchema>;

View File

@ -0,0 +1,173 @@
import { Caveat } from 'next/font/google';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { ArrowRight } from 'lucide-react';
import { redis } from '@documenso/lib/server-only/redis';
import { stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { PasswordReveal } from '~/components/(marketing)/password-reveal';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
});
export type ClaimedPlanPageProps = {
searchParams?: {
sessionId?: string;
};
};
export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlanPageProps) {
const { sessionId } = searchParams;
const session = await stripe.checkout.sessions.retrieve(sessionId as string);
const user = await prisma.user.findFirst({
where: {
id: Number(session.client_reference_id),
},
});
if (!user) {
redirect('/');
}
const signatureText = session.metadata?.signatureText || user.name;
let signatureDataUrl = '';
if (session.metadata?.signatureDataUrl) {
const result = await redis.get<string>(`signature:${session.metadata.signatureDataUrl}`);
if (result) {
signatureDataUrl = result;
}
}
const password = await redis.get<string>(`user:${user.id}:temp-password`);
return (
<div className="mt-12">
<h1 className="text-3xl font-bold text-slate-900 md:text-4xl">
Welcome to the <span className="text-primary">open signing</span> revolution{' '}
<u>{user.name}</u>
</h1>
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg">
It's not every day you get to be part of a revolution.
</p>
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg">
But today is that day, by signing up to Documenso, you're joining a movement of people who
want to make the world a better place.
</p>
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg">
We're going to change the way people sign documents. We're going to make it easier, faster,
and more secure. And we're going to do it together.
</p>
<div className="mt-12">
<h2 className="text-2xl font-bold text-slate-900">Let's do it together</h2>
<div className="-mx-4 mt-8 flex md:-mx-8">
<div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none">
<p
className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl',
fontCaveat.className,
)}
>
Timur
</p>
<p className="text-sm text-slate-500 md:text-lg">
Timur Ercan
<span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span>
Co Founder
</p>
</div>
<div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none">
<p
className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl',
fontCaveat.className,
)}
>
Lucas
</p>
<p className="text-sm text-slate-500 md:text-lg">
Lucas Smith
<span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span>
Co Founder
</p>
</div>
<div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none">
{signatureDataUrl && (
<img src={signatureDataUrl} alt="your-signature" className="max-w-[172px]" />
)}
{!signatureDataUrl && (
<p
className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl',
fontCaveat.className,
)}
>
{signatureText}
</p>
)}
<p className="text-sm text-slate-500 md:text-lg">
{user.name}
<span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span>
Our new favourite customer
</p>
</div>
</div>
</div>
<div className="mt-12">
<h2 className="text-2xl font-bold text-slate-900">Your sign in details</h2>
<div className="mt-4">
<p className="text-lg text-slate-500">
<span className="font-bold">Email:</span> {user.email}
</p>
<p className="mt-2 text-lg text-slate-500">
<span className="font-bold">Password:</span>{' '}
<PasswordReveal password={password ?? 'password'} />
</p>
</div>
<p className="mt-4 text-sm italic text-slate-500">
This is a temporary password. Please change it as soon as possible.
</p>
<Link
// eslint-disable-next-line turbo/no-undeclared-env-vars
href={`${process.env.NEXT_PUBLIC_APP_URL}/login`}
target="_blank"
className="mt-4 block"
>
<Button size="lg" className="text-base">
Let's get started!
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import { Footer } from '~/components/(marketing)/footer';
import { Header } from '~/components/(marketing)/header';
export type MarketingLayoutProps = {
children: React.ReactNode;
};
export default function MarketingLayout({ children }: MarketingLayoutProps) {
return (
<div className="relative max-w-[100vw] overflow-y-auto overflow-x-hidden pt-20 md:pt-28">
<div className="fixed left-0 top-0 z-50 w-full bg-white/50 backdrop-blur-md">
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
</div>
<div className="relative mx-auto max-w-screen-xl px-4 lg:px-8">{children}</div>
<Footer className="mt-24 bg-transparent backdrop-blur-[2px]" />
</div>
);
}

View File

@ -0,0 +1,31 @@
/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */
import { Caveat } from 'next/font/google';
import { cn } from '@documenso/ui/lib/utils';
import { Callout } from '~/components/(marketing)/callout';
import { FasterSmarterBeautifulBento } from '~/components/(marketing)/faster-smarter-beautiful-bento';
import { Hero } from '~/components/(marketing)/hero';
import { OpenBuildTemplateBento } from '~/components/(marketing)/open-build-template-bento';
import { ShareConnectPaidWidgetBento } from '~/components/(marketing)/share-connect-paid-widget-bento';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
export default async function IndexPage() {
return (
<div className={cn('mt-12', fontCaveat.variable)}>
<Hero />
<FasterSmarterBeautifulBento className="my-48" />
<ShareConnectPaidWidgetBento className="my-48" />
<OpenBuildTemplateBento className="my-48" />
<Callout />
</div>
);
}

View File

@ -0,0 +1,163 @@
import Link from 'next/link';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@documenso/ui/primitives/accordion';
import { PricingTable } from '~/components/(marketing)/pricing-table';
export type PricingPageProps = {
searchParams?: {
planId?: string;
email?: string;
name?: string;
cancelled?: string;
};
};
export default function PricingPage() {
return (
<div className="mt-12">
<div className="text-center">
<h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1>
<p className="mt-4 text-lg leading-normal text-[#31373D]">
Designed for every stage of your journey.
</p>
<p className="text-lg leading-normal text-[#31373D]">Get started today.</p>
</div>
<div className="mt-12">
<PricingTable />
</div>
<div className="mx-auto mt-36 max-w-2xl">
{/* FAQ Section */}
<h2 className="text-4xl font-semibold">FAQs</h2>
<Accordion type="multiple" className="mt-8">
<AccordionItem value="plan-differences">
<AccordionTrigger className="text-left text-lg font-semibold">
What is the difference between the plans?
</AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
You can self-host Documenso for free or use our ready-to-use hosted version. The
hosted version comes with additional support, painless scalability and more. Early
adopters of the community plan will get access to all features we build this year, for
no additional cost! Forever! Yes, that includes multiple users per account later. If
you want Documenso for your enterprise, we are happy to talk about your needs.
</AccordionContent>
</AccordionItem>
<AccordionItem value="data-handling">
<AccordionTrigger className="text-left text-lg font-semibold">
How do you handle my data?
</AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
Securely. Our data centers are located in Frankfurt (Germany), giving us the best
local privacy laws. We are very aware of the sensitive nature of our data and follow
best practices to ensure the security and integrity of the data entrusted to us.
</AccordionContent>
</AccordionItem>
<AccordionItem value="should-use-cloud">
<AccordionTrigger className="text-left text-lg font-semibold">
Why should I use your hosting service?
</AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
Using our hosted version is the easiest way to get started, you can simply subscribe
and start signing your documents. We take care of the infrastructure, so you can focus
on your business. Additionally, when using our hosted version you benefit from our
trusted signing certificates which helps you to build trust with your customers.
</AccordionContent>
</AccordionItem>
<AccordionItem value="how-to-contribute">
<AccordionTrigger className="text-left text-lg font-semibold">
How can I contribute?
</AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
That's awesome. You can take a look at the current{' '}
<Link
className="text-documenso-700 font-bold"
href="https://github.com/documenso/documenso/milestones"
target="_blank"
>
Issues
</Link>{' '}
and join our{' '}
<Link
className="text-documenso-700 font-bold"
href="https://join.slack.com/t/documenso/shared_invite/zt-1vibm8txi-DqsDFtdp44Hn2H5lc~RpPQ"
target="_blank"
>
Slack Community
</Link>{' '}
to keep up to date, on what the current priorities are. In any case, we are an open
community and welcome all input, technical and non-technical ❤️
</AccordionContent>
</AccordionItem>
<AccordionItem value="can-i-use-documenso-commercially">
<AccordionTrigger className="text-left text-lg font-semibold">
Can I use Documenso commercially?
</AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
Yes! Documenso is offered under the GNU AGPL V3 open source license. This means you
can use it for free and even modify it to fit your needs, as long as you publish your
changes under the same license.
</AccordionContent>
</AccordionItem>
<AccordionItem value="why-prefer-documenso">
<AccordionTrigger className="text-left text-lg font-semibold">
Why should I prefer Documenso over DocuSign or some other signing tool?
</AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
Documenso is a community effort to create an open and vibrant ecosystem around a tool,
everybody is free to use and adapt. By being truly open we want to create trusted
infrastructure for the future of the internet.
</AccordionContent>
</AccordionItem>
<AccordionItem value="where-can-i-get-support">
<AccordionTrigger className="text-left text-lg font-semibold">
Where can I get support?
</AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
We are happy to assist you at{' '}
<Link
className="text-documenso-700 font-bold"
target="_blank"
href="mailto:support@documenso.com"
>
support@documenso.com
</Link>{' '}
or{' '}
<a
className="text-documenso-700 font-bold"
href="https://join.slack.com/t/documenso/shared_invite/zt-1vibm8txi-DqsDFtdp44Hn2H5lc~RpPQ"
target="_blank"
>
in our Slack-Support-Channel
</a>{' '}
please message either Lucas or Timur to get added to the channel if you are not
already a member.
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
);
}

View File

@ -0,0 +1 @@
@import '@documenso/ui/styles/theme.css';

View File

@ -0,0 +1,53 @@
import { Inter } from 'next/font/google';
import { Toaster } from '@documenso/ui/primitives/toaster';
import { PlausibleProvider } from '~/providers/plausible';
import './globals.css';
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
export const metadata = {
title: 'Documenso - The Open Source DocuSign Alternative',
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
keywords:
'Documenso, open source, DocuSign alternative, document signing, open signing infrastructure, open-source community, fast signing, beautiful signing, smart templates',
authors: { name: 'Documenso, Inc.' },
robots: 'index, follow',
openGraph: {
title: 'Documenso - The Open Source DocuSign Alternative',
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
type: 'website',
images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`],
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={fontInter.variable} suppressHydrationWarning>
<head>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
</head>
<body>
<PlausibleProvider>
{children}
</PlausibleProvider>
<Toaster />
</body>
</html>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

View File

@ -0,0 +1,56 @@
'use client';
import Link from 'next/link';
import { Github } from 'lucide-react';
import { usePlausible } from 'next-plausible';
import { Button } from '@documenso/ui/primitives/button';
export const Callout = () => {
const event = usePlausible();
const onSignUpClick = () => {
const el = document.getElementById('email');
if (el) {
const { top } = el.getBoundingClientRect();
window.scrollTo({
top: top - 120,
behavior: 'smooth',
});
setTimeout(() => {
el.focus();
}, 500);
}
};
return (
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-4">
<Button
type="button"
variant="outline"
className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick}
>
Get the Community Plan
<span className="bg-primary -mr-2 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever!
</span>
</Button>
<Link
href="https://github.com/documenso/documenso"
target="_blank"
onClick={() => event('view-github')}
>
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
<Github className="mr-2 h-5 w-5" />
Star on Github
</Button>
</Link>
</div>
);
};

View File

@ -0,0 +1,148 @@
'use client';
import React, { useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { Info, Loader } from 'lucide-react';
import { usePlausible } from 'next-plausible';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { claimPlan } from '~/api/claim-plan/fetcher';
import { FormErrorMessage } from '../form/form-error-message';
export const ZClaimPlanDialogFormSchema = z.object({
name: z.string().min(3),
email: z.string().email(),
});
export type TClaimPlanDialogFormSchema = z.infer<typeof ZClaimPlanDialogFormSchema>;
export type ClaimPlanDialogProps = {
className?: string;
planId: string;
children: React.ReactNode;
};
export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => {
const params = useSearchParams();
const { toast } = useToast();
const event = usePlausible();
const [open, setOpen] = useState(() => params?.get('cancelled') === 'true');
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<TClaimPlanDialogFormSchema>({
mode: 'onBlur',
defaultValues: {
name: params?.get('name') ?? '',
email: params?.get('email') ?? '',
},
resolver: zodResolver(ZClaimPlanDialogFormSchema),
});
const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => {
try {
const delay = new Promise<void>((resolve) => setTimeout(resolve, 1000));
const [redirectUrl] = await Promise.all([
claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }),
delay,
]);
event('claim-plan-pricing');
window.location.href = redirectUrl;
} catch (error) {
event('claim-plan-failed');
toast({
title: 'Something went wrong',
description: error instanceof Error ? error.message : 'Please try again later.',
variant: 'destructive',
});
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Claim your plan</DialogTitle>
<DialogDescription className="mt-4">
We're almost there! Please enter your email address and name to claim your plan.
</DialogDescription>
</DialogHeader>
<form
className={cn('flex flex-col gap-y-4', className)}
onSubmit={handleSubmit(onFormSubmit)}
>
{params?.get('cancelled') === 'true' && (
<div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<Info className="h-5 w-5 text-yellow-400" />
</div>
<div className="ml-3">
<p className="text-sm leading-5 text-yellow-700">
You have cancelled the payment process. If you didn't mean to do this, please
try again.
</p>
</div>
</div>
</div>
)}
<div>
<Label className="text-slate-500">Name</Label>
<Input type="text" className="mt-2" {...register('name')} autoFocus />
<FormErrorMessage className="mt-1" error={errors.name} />
</div>
<div>
<Label className="text-slate-500">Email</Label>
<Input type="email" className="mt-2" {...register('email')} />
<FormErrorMessage className="mt-1" error={errors.email} />
</div>
<Button type="submit" size="lg" disabled={isSubmitting}>
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
Claim the Community Plan ({/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
? 'Monthly'
: 'Yearly'}
)
</Button>
</form>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,77 @@
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import backgroundPattern from '~/assets/background-pattern.png';
import cardBeautifulFigure from '~/assets/card-beautiful-figure.png';
import cardFastFigure from '~/assets/card-fast-figure.png';
import cardSmartFigure from '~/assets/card-smart-figure.png';
export type FasterSmarterBeautifulBentoProps = HTMLAttributes<HTMLDivElement>;
export const FasterSmarterBeautifulBento = ({
className,
...props
}: FasterSmarterBeautifulBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}
alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
/>
</div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
A 10x better signing experience.
<span className="block md:mt-0">Faster, smarter and more beautiful.</span>
</h2>
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2" degrees={45} gradient>
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
<p className="col-span-12 leading-relaxed text-[#555E67] lg:col-span-6">
<strong className="block">Fast.</strong>
When it comes to sending or receiving a contract, you can count on lightning-fast
speeds.
</p>
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
<Image src={cardFastFigure} alt="its fast" className="max-w-[80%] lg:max-w-none" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Beautiful.</strong>
Because signing should be celebrated. Thats why we care about the smallest detail in
our product.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardBeautifulFigure} alt="its fast" className="w-full max-w-xs" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Smart.</strong>
Our custom templates come with smart rules that can help you save time and energy.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardSmartFigure} alt="its fast" className="w-full max-w-[16rem]" />
</div>
</CardContent>
</Card>
</div>
</div>
);
};

View File

@ -0,0 +1,86 @@
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { Github, Slack, Twitter } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
export type FooterProps = HTMLAttributes<HTMLDivElement>;
export const Footer = ({ className, ...props }: FooterProps) => {
return (
<div className={cn('border-t py-12', className)} {...props}>
<div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8">
<div>
<Link href="/">
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
</Link>
<div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4 text-[#8D8D8D]">
<Link
href="https://twitter.com/documenso"
target="_blank"
className="hover:text-[#6D6D6D]"
>
<Twitter className="h-6 w-6" />
</Link>
<Link
href="https://github.com/documenso/documenso"
target="_blank"
className="hover:text-[#6D6D6D]"
>
<Github className="h-6 w-6" />
</Link>
<Link
href="https://documenso.slack.com"
target="_blank"
className="hover:text-[#6D6D6D]"
>
<Slack className="h-6 w-6" />
</Link>
</div>
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2.5">
<Link
href="/pricing"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Pricing
</Link>
<Link
href="https://status.documenso.com"
target="_blank"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Status
</Link>
<Link
href="mailto:support@documenso.com"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Support
</Link>
{/* <Link
href="/privacy"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Privacy
</Link> */}
</div>
</div>
<div className="mx-auto mt-4 w-full max-w-screen-xl px-8 md:mt-12 lg:mt-24">
<p className="text-sm text-[#8D8D8D]">
© {new Date().getFullYear()} Documenso, Inc. All rights reserved.
</p>
</div>
</div>
);
};

View File

@ -0,0 +1,32 @@
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { cn } from '@documenso/ui/lib/utils';
export type HeaderProps = HTMLAttributes<HTMLElement>;
export const Header = ({ className, ...props }: HeaderProps) => {
return (
<header className={cn('flex items-center justify-between', className)} {...props}>
<Link href="/">
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
</Link>
<div className="flex items-center gap-x-6">
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
Pricing
</Link>
<Link
href="https://app.documenso.com/login"
target="_blank"
className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Sign in
</Link>
</div>
</header>
);
};

View File

@ -0,0 +1,217 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { Variants, motion } from 'framer-motion';
import { Github } from 'lucide-react';
import { usePlausible } from 'next-plausible';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import backgroundPattern from '~/assets/background-pattern.png';
import { Widget } from './widget';
export type HeroProps = {
className?: string;
[key: string]: unknown;
};
const BackgroundPatternVariants: Variants = {
initial: {
opacity: 0,
},
animate: {
opacity: 1,
transition: {
delay: 1,
duration: 1.2,
},
},
};
const HeroTitleVariants: Variants = {
initial: {
opacity: 0,
y: 60,
},
animate: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
};
export const Hero = ({ className, ...props }: HeroProps) => {
const event = usePlausible();
const onSignUpClick = () => {
const el = document.getElementById('email');
if (el) {
const { top } = el.getBoundingClientRect();
window.scrollTo({
top: top - 120,
behavior: 'smooth',
});
requestAnimationFrame(() => {
el.focus();
});
}
};
return (
<motion.div className={cn('relative', className)} {...props}>
<div className="absolute -inset-24 -z-10">
<motion.div
className="flex h-full w-full origin-top-right items-center justify-center"
variants={BackgroundPatternVariants}
initial="initial"
animate="animate"
>
<Image
src={backgroundPattern}
alt="background pattern"
className="-mr-[50vw] -mt-[15vh] h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
/>
</motion.div>
</div>
<div className="relative">
<motion.h2
variants={HeroTitleVariants}
initial="initial"
animate="animate"
className="text-center text-4xl font-bold leading-tight tracking-tight lg:text-[64px]"
>
Document signing,
<span className="block" /> finally open source.
</motion.h2>
<motion.div
variants={HeroTitleVariants}
initial="initial"
animate="animate"
className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-4"
>
<Button
type="button"
variant="outline"
className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick}
>
Get the Community Plan
<span className="bg-primary -mr-2 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever!
</span>
</Button>
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
<Github className="mr-2 h-5 w-5" />
Star on Github
</Button>
</Link>
</motion.div>
<motion.div
variants={HeroTitleVariants}
initial="initial"
animate="animate"
className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-4"
>
<Link
href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso"
target="_blank"
>
<img
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily"
alt="Documenso - The open source DocuSign alternative | Product Hunt"
style={{ width: '250px', height: '54px' }}
/>
</Link>
</motion.div>
<motion.div
className="mt-12"
variants={{
initial: {
scale: 0.2,
opacity: 0,
},
animate: {
scale: 1,
opacity: 1,
transition: {
ease: 'easeInOut',
delay: 0.5,
duration: 0.8,
},
},
}}
initial="initial"
animate="animate"
>
<Widget className="mt-12">
<strong>Documenso Supporter Pledge</strong>
<p className="w-full max-w-[70ch]">
Our mission is to create an open signing infrastructure that empowers the world,
enabling businesses to embrace openness, cooperation, and transparency. We believe
that signing, as a fundamental act, should embody these values. By offering an
open-source signing solution, we aim to make document signing accessible, transparent,
and trustworthy.
</p>
<p className="w-full max-w-[70ch]">
Through our platform, called Documenso, we strive to earn your trust by allowing
self-hosting and providing complete visibility into its inner workings. We value
inclusivity and foster an environment where diverse perspectives and contributions are
welcomed, even though we may not implement them all.
</p>
<p className="w-full max-w-[70ch]">
At Documenso, we envision a web-enabled future for business and contracts, and we are
committed to being the leading provider of open signing infrastructure. By combining
exceptional product design with open-source principles, we aim to deliver a robust and
well-designed application that exceeds your expectations.
</p>
<p className="w-full max-w-[70ch]">
We understand that exceptional products are born from exceptional communities, and we
invite you to join our open-source community. Your contributions, whether technical or
non-technical, will help shape the future of signing. Together, we can create a better
future for everyone.
</p>
<p className="w-full max-w-[70ch]">
Today we invite you to join us on this journey: By signing this mission statement you
signal your support of Documenso's mission{' '}
<span className="bg-primary text-black">
(in a non-legally binding, but heartfelt way)
</span>{' '}
and lock in the early supporter plan for forever, including everything we build this
year.
</p>
<div className="flex h-24 items-center">
<p className={cn('text-5xl [font-family:var(--font-caveat)]')}>Timur & Lucas</p>
</div>
<div>
<strong>Timur Ercan & Lucas Smith</strong>
<p className="mt-1">Co-Founders, Documenso</p>
</div>
</Widget>
</motion.div>
</div>
</motion.div>
);
};

View File

@ -0,0 +1,74 @@
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import backgroundPattern from '~/assets/background-pattern.png';
import cardBuildFigure from '~/assets/card-build-figure.png';
import cardOpenFigure from '~/assets/card-open-figure.png';
import cardTemplateFigure from '~/assets/card-template-figure.png';
export type OpenBuildTemplateBentoProps = HTMLAttributes<HTMLDivElement>;
export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplateBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}
alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
/>
</div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
Truly your own.
<span className="block md:mt-0">Customise and expand.</span>
</h2>
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2" degrees={45} gradient>
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
<p className="col-span-12 leading-relaxed text-[#555E67] lg:col-span-6">
<strong className="block">Open Source or Hosted.</strong>
Its up to you. Either clone our repository or rely on our easy to use hosting
solution.
</p>
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
<Image src={cardOpenFigure} alt="its fast" className="max-w-[80%] lg:max-w-full" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Build on top.</strong>
Make it your own through advanced customization and adjustability.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardBuildFigure} alt="its fast" className="w-full max-w-xs" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Template Store (Soon).</strong>
Choose a template from the community app store. Or submit your own template for others
to use.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardTemplateFigure} alt="its fast" className="w-full max-w-sm" />
</div>
</CardContent>
</Card>
</div>
</div>
);
};

View File

@ -0,0 +1,33 @@
'use client';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
export type PasswordRevealProps = {
password: string;
};
export const PasswordReveal = ({ password }: PasswordRevealProps) => {
const { toast } = useToast();
const [, copy] = useCopyToClipboard();
const onCopyClick = () => {
copy(password).then(() => {
toast({
title: 'Copied to clipboard',
description: 'Your password has been copied to your clipboard.',
});
});
};
return (
<button
type="button"
className="px-2 blur-sm hover:opacity-50 hover:blur-none"
onClick={onCopyClick}
>
{password}
</button>
);
};

View File

@ -0,0 +1,179 @@
'use client';
import { HTMLAttributes, useMemo, useState } from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { AnimatePresence, motion } from 'framer-motion';
import { usePlausible } from 'next-plausible';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { ClaimPlanDialog } from './claim-plan-dialog';
export type PricingTableProps = HTMLAttributes<HTMLDivElement>;
const SELECTED_PLAN_BAR_LAYOUT_ID = 'selected-plan-bar';
export const PricingTable = ({ className, ...props }: PricingTableProps) => {
const params = useSearchParams();
const event = usePlausible();
const [period, setPeriod] = useState<'MONTHLY' | 'YEARLY'>(() =>
// eslint-disable-next-line turbo/no-undeclared-env-vars
params?.get('planId') === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID
? 'YEARLY'
: 'MONTHLY',
);
const planId = useMemo(() => {
if (period === 'MONTHLY') {
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
}
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID;
}, [period]);
return (
<div className={cn('', className)} {...props}>
<div className="flex items-center justify-center gap-x-6">
<AnimatePresence>
<motion.button
key="MONTHLY"
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', {
'text-slate-900': period === 'MONTHLY',
'hover:text-slate-900/80': period !== 'MONTHLY',
})}
onClick={() => setPeriod('MONTHLY')}
>
Monthly
{period === 'MONTHLY' && (
<motion.div
layoutId={SELECTED_PLAN_BAR_LAYOUT_ID}
className="bg-primary absolute bottom-0 left-0 h-[3px] w-full rounded-full"
/>
)}
</motion.button>
<motion.button
key="YEARLY"
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', {
'text-slate-900': period === 'YEARLY',
'hover:text-slate-900/80': period !== 'YEARLY',
})}
onClick={() => setPeriod('YEARLY')}
>
Yearly
<div className="block rounded-full bg-slate-200 px-2 py-0.5 text-xs text-slate-700">
Save $60
</div>
{period === 'YEARLY' && (
<motion.div
layoutId={SELECTED_PLAN_BAR_LAYOUT_ID}
className="bg-primary absolute bottom-0 left-0 h-[3px] w-full rounded-full"
/>
)}
</motion.button>
</AnimatePresence>
</div>
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 md:grid-cols-2 lg:grid-cols-3">
<div
data-plan="self-hosted"
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5"
>
<p className="text-4xl font-medium text-slate-900">Self Hosted</p>
<p className="text-primary mt-2.5 text-xl font-medium">Free</p>
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
For small teams and individuals who need a simple solution
</p>
<Link
href="https://github.com/documenso/documenso"
target="_blank"
className="mt-6"
onClick={() => event('view-github')}
>
<Button className="rounded-full text-base">View on Github</Button>
</Link>
<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 text-slate-900">Full Control</p>
<p className="py-4 text-slate-900">Customizability</p>
<p className="py-4 text-slate-900">Docker Ready</p>
<p className="py-4 text-slate-900">Community Support</p>
<p className="py-4 text-slate-900">Free, Forever</p>
</div>
</div>
<div
data-plan="community"
className="border-primary flex flex-col items-center justify-center rounded-lg border-2 bg-white px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380] shadow-slate-900/5"
>
<p className="text-4xl font-medium text-slate-900">Community</p>
<div className="text-primary mt-2.5 text-xl font-medium">
<AnimatePresence mode="wait">
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
{period === 'YEARLY' && <motion.div layoutId="pricing">$300</motion.div>}
</AnimatePresence>
</div>
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
For fast-growing companies that aim to scale across multiple teams.
</p>
<ClaimPlanDialog planId={planId}>
<Button className="mt-6 rounded-full text-base">Signup Now</Button>
</ClaimPlanDialog>
<div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Documenso Early Adopter Deal:</p>
<p className="py-4 text-slate-900">Join the movement</p>
<p className="py-4 text-slate-900">Simple signing solution</p>
<p className="py-4 text-slate-900">Email and Slack assistance</p>
<p className="py-4 text-slate-900">
<strong>Includes all upcoming features</strong>
</p>
<p className="py-4 text-slate-900">Fixed, straightforward pricing</p>
</div>
</div>
<div
data-plan="enterprise"
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5"
>
<p className="text-4xl font-medium text-slate-900">Enterprise</p>
<p className="text-primary mt-2.5 text-xl font-medium">Pricing on request</p>
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
For large organizations that need extra flexibility and control.
</p>
<Link
href="https://dub.sh/enterprise"
target="_blank"
className="mt-6"
onClick={() => event('enterprise-contact')}
>
<Button className="rounded-full text-base">Contact Us</Button>
</Link>
<div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Everything in Community, plus:</p>
<p className="py-4 text-slate-900">Custom Subdomain</p>
<p className="py-4 text-slate-900">Compliance Check</p>
<p className="py-4 text-slate-900">Guaranteed Uptime</p>
<p className="py-4 text-slate-900">Reporting & Analysis</p>
<p className="py-4 text-slate-900">24/7 Support</p>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,91 @@
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import backgroundPattern from '~/assets/background-pattern.png';
import cardConnectionsFigure from '~/assets/card-connections-figure.png';
import cardPaidFigure from '~/assets/card-paid-figure.png';
import cardSharingFigure from '~/assets/card-sharing-figure.png';
import cardWidgetFigure from '~/assets/card-widget-figure.png';
export type ShareConnectPaidWidgetBentoProps = HTMLAttributes<HTMLDivElement>;
export const ShareConnectPaidWidgetBento = ({
className,
...props
}: ShareConnectPaidWidgetBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}
alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
/>
</div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
Integrates with all your favourite tools.
<span className="block md:mt-0">Send, connect, receive and embed everywhere.</span>
</h2>
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2 lg:col-span-1" degrees={120} gradient>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Easy Sharing (Soon).</strong>
Receive your personal link to share with everyone you care about.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardSharingFigure} alt="its fast" className="w-full max-w-xs" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Connections (Soon).</strong>
Create connections and automations with Zapier and more to integrate with your
favorite tools.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardConnectionsFigure} alt="its fast" className="w-full max-w-sm" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Get paid (Soon).</strong>
Integrated payments with stripe so you dont have to worry about getting paid.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardPaidFigure} alt="its fast" className="w-full max-w-[14rem]" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">React Widget (Soon).</strong>
Easily embed Documenso into your product. Simply copy and paste our react widget into
your application.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardWidgetFigure} alt="its fast" className="w-full max-w-xs" />
</div>
</CardContent>
</Card>
</div>
</div>
);
};

View File

@ -0,0 +1,400 @@
'use client';
import { HTMLAttributes, KeyboardEvent, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { Loader } from 'lucide-react';
import { usePlausible } from 'next-plausible';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { claimPlan } from '~/api/claim-plan/fetcher';
import { FormErrorMessage } from '../form/form-error-message';
import { SignaturePad } from '../signature-pad';
const ZWidgetFormSchema = z
.object({
email: z.string().email({ message: 'Please enter a valid email address.' }),
name: z.string().min(3, { message: 'Please enter a valid name.' }),
})
.and(
z.union([
z.object({
signatureDataUrl: z.string().min(1),
signatureText: z.null().or(z.string().max(0)),
}),
z.object({
signatureDataUrl: z.null().or(z.string().max(0)),
signatureText: z.string().min(1),
}),
]),
);
export type TWidgetFormSchema = z.infer<typeof ZWidgetFormSchema>;
export type WidgetProps = HTMLAttributes<HTMLDivElement>;
export const Widget = ({ className, children, ...props }: WidgetProps) => {
const { toast } = useToast();
const event = usePlausible();
const [step, setStep] = useState<'EMAIL' | 'NAME' | 'SIGN'>('EMAIL');
const [showSigningDialog, setShowSigningDialog] = useState(false);
const [draftSignatureDataUrl, setDraftSignatureDataUrl] = useState<string | null>(null);
const {
control,
register,
handleSubmit,
setValue,
trigger,
watch,
formState: { errors, isSubmitting, isValid },
} = useForm<TWidgetFormSchema>({
mode: 'onChange',
defaultValues: {
email: '',
name: '',
signatureDataUrl: null,
signatureText: '',
},
resolver: zodResolver(ZWidgetFormSchema),
});
const signatureDataUrl = watch('signatureDataUrl');
const signatureText = watch('signatureText');
const stepsRemaining = useMemo(() => {
if (step === 'NAME') {
return 2;
}
if (step === 'SIGN') {
return 1;
}
return 3;
}, [step]);
const onNextStepClick = () => {
if (step === 'EMAIL') {
setStep('NAME');
setTimeout(() => {
document.querySelector<HTMLElement>('#name')?.focus();
}, 0);
}
if (step === 'NAME') {
setStep('SIGN');
setTimeout(() => {
document.querySelector<HTMLElement>('#signatureText')?.focus();
}, 0);
}
};
const onEnterPress = (callback: () => void) => {
return (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
callback();
}
};
};
const onSignatureConfirmClick = () => {
setValue('signatureDataUrl', draftSignatureDataUrl);
setValue('signatureText', '');
trigger('signatureDataUrl');
setShowSigningDialog(false);
};
const onFormSubmit = async ({
email,
name,
signatureDataUrl,
signatureText,
}: TWidgetFormSchema) => {
try {
const delay = new Promise<void>((resolve) => setTimeout(resolve, 1000));
// eslint-disable-next-line turbo/no-undeclared-env-vars
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
const claimPlanInput = signatureDataUrl
? {
name,
email,
planId,
signatureDataUrl: signatureDataUrl!,
signatureText: null,
}
: {
name,
email,
planId,
signatureDataUrl: null,
signatureText: signatureText!,
};
const [result] = await Promise.all([claimPlan(claimPlanInput), delay]);
event('claim-plan-widget');
window.location.href = result;
} catch (error) {
event('claim-plan-failed');
toast({
title: 'Something went wrong',
description: error instanceof Error ? error.message : 'Please try again later.',
variant: 'destructive',
});
}
};
return (
<>
<Card
className={cn('mx-auto w-full max-w-4xl rounded-3xl before:rounded-3xl', className)}
gradient
{...props}
>
<div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8">
<div className="col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed text-[#727272] lg:col-span-7">
{children}
</div>
<form
className="col-span-12 flex flex-col rounded-2xl bg-[#F7F7F7] p-6 lg:col-span-5"
onSubmit={handleSubmit(onFormSubmit)}
>
<h3 className="text-2xl font-semibold">Sign up for the community plan</h3>
<p className="mt-2 text-xs text-[#AFAFAF]">
with Timur Ercan & Lucas Smith from Documenso
</p>
<hr className="mb-6 mt-4" />
<AnimatePresence>
<motion.div key="email">
<label htmlFor="email" className="text-lg font-semibold text-slate-900 lg:text-xl">
Whats your email?
</label>
<Controller
control={control}
name="email"
render={({ field }) => (
<div className="relative mt-2">
<Input
id="email"
type="email"
placeholder=""
className="w-full bg-white pr-16"
disabled={isSubmitting}
onKeyDown={(e) =>
field.value !== '' &&
!errors.email?.message &&
onEnterPress(onNextStepClick)(e)
}
{...field}
/>
<div className="absolute inset-y-0 right-0 p-1.5">
<Button
type="button"
className="bg-primary h-full w-14 rounded"
disabled={!field.value || !!errors.email?.message}
onClick={() => onNextStepClick()}
>
Next
</Button>
</div>
</div>
)}
/>
<FormErrorMessage error={errors.email} className="mt-1" />
</motion.div>
{(step === 'NAME' || step === 'SIGN') && (
<motion.div
key="name"
className="mt-4"
animate={{
opacity: 1,
transform: 'translateX(0)',
}}
initial={{
opacity: 0,
transform: 'translateX(-25%)',
}}
exit={{
opacity: 0,
transform: 'translateX(25%)',
}}
>
<label htmlFor="name" className="text-lg font-semibold text-slate-900 lg:text-xl">
and your name?
</label>
<Controller
control={control}
name="name"
render={({ field }) => (
<div className="relative mt-2">
<Input
id="name"
type="text"
placeholder=""
className="w-full bg-white pr-16"
disabled={isSubmitting}
onKeyDown={(e) =>
field.value !== '' &&
!errors.name?.message &&
onEnterPress(onNextStepClick)(e)
}
{...field}
/>
<div className="absolute inset-y-0 right-0 p-1.5">
<Button
type="button"
className="bg-primary h-full w-14 rounded"
disabled={!field.value || !!errors.name?.message}
onClick={() => onNextStepClick()}
>
Next
</Button>
</div>
</div>
)}
/>
<FormErrorMessage error={errors.name} className="mt-1" />
</motion.div>
)}
</AnimatePresence>
<div className="mt-12 flex-1" />
<div className="flex items-center justify-between">
<p className="text-xs text-[#AFAFAF]">{stepsRemaining} step(s) until signed</p>
<p className="block text-xs text-[#AFAFAF] md:hidden">Minimise contract</p>
</div>
<div className="relative mt-2.5 h-[2px] w-full bg-[#E9E9E9]">
<div
className={cn('bg-primary/60 absolute inset-y-0 left-0 duration-200', {
'w-1/3': stepsRemaining === 3,
'w-2/3': stepsRemaining === 2,
'w-11/12': stepsRemaining === 1,
})}
/>
</div>
<Card id="signature" className="mt-4" degrees={-140} gradient>
<CardContent
role="button"
className="relative cursor-pointer pt-6"
onClick={() => setShowSigningDialog(true)}
>
<div className="flex h-28 items-center justify-center pb-6">
{!signatureText && signatureDataUrl && (
<img src={signatureDataUrl} alt="user signature" className="h-full" />
)}
{signatureText && (
<p
className={cn(
'text-4xl font-semibold text-slate-900 [font-family:var(--font-caveat)]',
)}
>
{signatureText}
</p>
)}
</div>
<div
className="absolute inset-x-0 bottom-0 flex cursor-auto items-center justify-between px-4 pb-2"
onClick={(e) => e.stopPropagation()}
>
<Input
id="signatureText"
className="border-none p-0 text-sm text-slate-700 placeholder:text-[#D6D6D6] focus-visible:ring-0"
placeholder="Draw or type name here"
disabled={isSubmitting}
{...register('signatureText', {
onChange: (e) => {
if (e.target.value !== '') {
setValue('signatureDataUrl', null);
}
},
})}
/>
<Button
type="submit"
className="h-8 disabled:bg-[#ECEEED] disabled:text-[#C6C6C6] disabled:hover:bg-[#ECEEED]"
disabled={!isValid || isSubmitting}
>
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
Sign
</Button>
</div>
</CardContent>
</Card>
</form>
</div>
</Card>
<Dialog open={showSigningDialog} onOpenChange={setShowSigningDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add your signature</DialogTitle>
</DialogHeader>
<DialogDescription>
By signing you signal your support of Documenso's mission in a <br></br>
<strong>non-legally binding, but heartfelt way</strong>. <br></br>
<br></br>You also unlock the option to purchase the early supporter plan including
everything we build this year for fixed price.
</DialogDescription>
<SignaturePad
className="aspect-video w-full rounded-md border"
onChange={setDraftSignatureDataUrl}
/>
<DialogFooter>
<Button variant="ghost" onClick={() => setShowSigningDialog(false)}>
Cancel
</Button>
<Button onClick={() => onSignatureConfirmClick()}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@ -0,0 +1,35 @@
import { AnimatePresence, motion } from 'framer-motion';
import { FieldError } from 'react-hook-form';
import { cn } from '@documenso/ui/lib/utils';
export type FormErrorMessageProps = {
className?: string;
error: FieldError | undefined;
};
export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) => {
return (
<AnimatePresence>
{error && (
<motion.p
initial={{
opacity: 0,
y: -10,
}}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: 10,
}}
className={cn('text-xs text-red-500', className)}
>
{error.message}
</motion.p>
)}
</AnimatePresence>
);
};

View File

@ -0,0 +1,321 @@
import { Point } from './point';
export class Canvas {
private readonly $canvas: HTMLCanvasElement;
private readonly $offscreenCanvas: HTMLCanvasElement;
private currentCanvasWidth = 0;
private currentCanvasHeight = 0;
private points: Point[] = [];
private onChangeHandlers: Array<(_canvas: Canvas, _cleared: boolean) => void> = [];
private isPressed = false;
private lastVelocity = 0;
private readonly VELOCITY_FILTER_WEIGHT = 0.5;
private readonly DPI = 2;
constructor(canvas: HTMLCanvasElement) {
this.$canvas = canvas;
this.$offscreenCanvas = document.createElement('canvas');
const { width, height } = this.$canvas.getBoundingClientRect();
this.currentCanvasWidth = width * this.DPI;
this.currentCanvasHeight = height * this.DPI;
this.$canvas.width = this.currentCanvasWidth;
this.$canvas.height = this.currentCanvasHeight;
Object.assign(this.$canvas.style, {
touchAction: 'none',
msTouchAction: 'none',
userSelect: 'none',
});
window.addEventListener('resize', this.onResize.bind(this));
this.$canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
this.$canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
this.$canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
this.$canvas.addEventListener('mouseenter', this.onMouseEnter.bind(this));
this.$canvas.addEventListener('mouseleave', this.onMouseLeave.bind(this));
this.$canvas.addEventListener('pointerdown', this.onMouseDown.bind(this));
this.$canvas.addEventListener('pointermove', this.onMouseMove.bind(this));
this.$canvas.addEventListener('pointerup', this.onMouseUp.bind(this));
}
/**
* Calculates the minimum stroke width as a percentage of the current canvas suitable for a signature.
*/
private minStrokeWidth(): number {
return Math.min(this.currentCanvasWidth, this.currentCanvasHeight) * 0.005;
}
/**
* Calculates the maximum stroke width as a percentage of the current canvas suitable for a signature.
*/
private maxStrokeWidth(): number {
return Math.min(this.currentCanvasWidth, this.currentCanvasHeight) * 0.035;
}
/**
* Retrieves the HTML canvas element.
*/
public getCanvas(): HTMLCanvasElement {
return this.$canvas;
}
/**
* Retrieves the 2D rendering context of the canvas.
* Throws an error if the context is not available.
*/
public getContext(): CanvasRenderingContext2D {
const ctx = this.$canvas.getContext('2d');
if (!ctx) {
throw new Error('Canvas context is not available.');
}
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
return ctx;
}
/**
* Handles the resize event of the canvas.
* Adjusts the canvas size and preserves the content using image data.
*/
private onResize(): void {
const { width, height } = this.$canvas.getBoundingClientRect();
const oldWidth = this.currentCanvasWidth;
const oldHeight = this.currentCanvasHeight;
const ctx = this.getContext();
const imageData = ctx.getImageData(0, 0, oldWidth, oldHeight);
this.$canvas.width = width * this.DPI;
this.$canvas.height = height * this.DPI;
this.currentCanvasWidth = width * this.DPI;
this.currentCanvasHeight = height * this.DPI;
ctx.putImageData(imageData, 0, 0, 0, 0, width * this.DPI, height * this.DPI);
}
/**
* Handles the mouse down event on the canvas.
* Adds the starting point for the signature.
*/
private onMouseDown(event: MouseEvent | PointerEvent | TouchEvent): void {
if (event.cancelable) {
event.preventDefault();
}
this.isPressed = true;
const point = Point.fromEvent(event, this.DPI);
this.addPoint(point);
}
/**
* Handles the mouse move event on the canvas.
* Adds a point to the signature if the mouse is pressed, based on the sample rate.
*/
private onMouseMove(event: MouseEvent | PointerEvent | TouchEvent): void {
if (event.cancelable) {
event.preventDefault();
}
if (!this.isPressed) {
return;
}
const point = Point.fromEvent(event, this.DPI);
if (point.distanceTo(this.points[this.points.length - 1]) > 10) {
this.addPoint(point);
}
}
/**
* Handles the mouse up event on the canvas.
* Adds the final point for the signature and resets the points array.
*/
private onMouseUp(event: MouseEvent | PointerEvent | TouchEvent, addPoint = true): void {
if (event.cancelable) {
event.preventDefault();
}
this.isPressed = false;
const point = Point.fromEvent(event, this.DPI);
if (addPoint) {
this.addPoint(point);
}
this.onChangeHandlers.forEach((handler) => handler(this, false));
this.points = [];
}
private onMouseEnter(event: MouseEvent): void {
if (event.cancelable) {
event.preventDefault();
}
event.buttons === 1 && this.onMouseDown(event);
}
private onMouseLeave(event: MouseEvent): void {
if (event.cancelable) {
event.preventDefault();
}
this.onMouseUp(event, false);
}
/**
* Adds a point to the signature and performs smoothing and drawing.
*/
private addPoint(point: Point): void {
const lastPoint = this.points[this.points.length - 1] ?? point;
this.points.push(point);
const smoothedPoints = this.smoothSignature(this.points);
let velocity = point.velocityFrom(lastPoint);
velocity =
this.VELOCITY_FILTER_WEIGHT * velocity +
(1 - this.VELOCITY_FILTER_WEIGHT) * this.lastVelocity;
const newWidth =
velocity > 0 && this.lastVelocity > 0 ? this.strokeWidth(velocity) : this.minStrokeWidth();
this.drawSmoothSignature(smoothedPoints, newWidth);
this.lastVelocity = velocity;
}
/**
* Applies a smoothing algorithm to the signature points.
*/
private smoothSignature(points: Point[]): Point[] {
const smoothedPoints: Point[] = [];
const startPoint = points[0];
const endPoint = points[points.length - 1];
smoothedPoints.push(startPoint);
for (let i = 0; i < points.length - 1; i++) {
const p0 = i > 0 ? points[i - 1] : startPoint;
const p1 = points[i];
const p2 = points[i + 1];
const p3 = i < points.length - 2 ? points[i + 2] : endPoint;
const cp1x = p1.x + (p2.x - p0.x) / 6;
const cp1y = p1.y + (p2.y - p0.y) / 6;
const cp2x = p2.x - (p3.x - p1.x) / 6;
const cp2y = p2.y - (p3.y - p1.y) / 6;
smoothedPoints.push(new Point(cp1x, cp1y));
smoothedPoints.push(new Point(cp2x, cp2y));
smoothedPoints.push(p2);
}
return smoothedPoints;
}
/**
* Draws the smoothed signature on the canvas.
*/
private drawSmoothSignature(points: Point[], width: number): void {
const ctx = this.getContext();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
const startPoint = points[0];
ctx.moveTo(startPoint.x, startPoint.y);
ctx.lineWidth = width;
for (let i = 1; i < points.length; i += 3) {
const cp1 = points[i];
const cp2 = points[i + 1];
const endPoint = points[i + 2];
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, endPoint.x, endPoint.y);
}
ctx.stroke();
ctx.closePath();
}
/**
* Calculates the stroke width based on the velocity.
*/
private strokeWidth(velocity: number): number {
return Math.max(this.maxStrokeWidth() / (velocity + 1), this.minStrokeWidth());
}
public registerOnChangeHandler(handler: (_canvas: Canvas, _cleared: boolean) => void): void {
this.onChangeHandlers.push(handler);
}
public unregisterOnChangeHandler(handler: (_canvas: Canvas, _cleared: boolean) => void): void {
this.onChangeHandlers = this.onChangeHandlers.filter((l) => l !== handler);
}
/**
* Retrieves the signature as a data URL.
*/
public toDataURL(type?: string, quality?: number): string {
return this.$canvas.toDataURL(type, quality);
}
/**
* Clears the signature from the canvas.
*/
public clear(): void {
const ctx = this.getContext();
ctx.clearRect(0, 0, this.currentCanvasWidth, this.currentCanvasHeight);
this.onChangeHandlers.forEach((handler) => handler(this, true));
this.points = [];
}
/**
* Retrieves the signature as an image blob.
*/
public toBlob(type?: string, quality?: number): Promise<Blob> {
return new Promise((resolve, reject) => {
this.$canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('Could not convert canvas to blob.'));
return;
}
resolve(blob);
},
type,
quality,
);
});
}
}

View File

@ -0,0 +1,29 @@
export const average = (a: number, b: number) => (a + b) / 2;
export const getSvgPathFromStroke = (points: number[][], closed = true) => {
const len = points.length;
if (len < 4) {
return ``;
}
let a = points[0];
let b = points[1];
const c = points[2];
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(2)},${b[1].toFixed(
2,
)} ${average(b[0], c[0]).toFixed(2)},${average(b[1], c[1]).toFixed(2)} T`;
for (let i = 2, max = len - 1; i < max; i++) {
a = points[i];
b = points[i + 1];
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(2)} `;
}
if (closed) {
result += 'Z';
}
return result;
};

View File

@ -0,0 +1 @@
export * from './signature-pad';

View File

@ -0,0 +1,98 @@
import {
MouseEvent as ReactMouseEvent,
PointerEvent as ReactPointerEvent,
TouchEvent as ReactTouchEvent,
} from 'react';
export type PointLike = {
x: number;
y: number;
timestamp: number;
};
const isTouchEvent = (
event:
| ReactMouseEvent
| ReactPointerEvent
| ReactTouchEvent
| MouseEvent
| PointerEvent
| TouchEvent,
): event is TouchEvent | ReactTouchEvent => {
return 'touches' in event;
};
export class Point implements PointLike {
public x: number;
public y: number;
public timestamp: number;
constructor(x: number, y: number, timestamp?: number) {
this.x = x;
this.y = y;
this.timestamp = timestamp ?? Date.now();
}
public distanceTo(point: PointLike): number {
return Math.sqrt(Math.pow(point.x - this.x, 2) + Math.pow(point.y - this.y, 2));
}
public equals(point: PointLike): boolean {
return this.x === point.x && this.y === point.y && this.timestamp === point.timestamp;
}
public velocityFrom(start: PointLike): number {
const timeDifference = this.timestamp - start.timestamp;
if (timeDifference !== 0) {
return this.distanceTo(start) / timeDifference;
}
return 0;
}
public static fromPointLike({ x, y, timestamp }: PointLike): Point {
return new Point(x, y, timestamp);
}
public static fromEvent(
event:
| ReactMouseEvent
| ReactPointerEvent
| ReactTouchEvent
| MouseEvent
| PointerEvent
| TouchEvent,
dpi = 1,
el?: HTMLElement | null,
): Point {
const target = el ?? event.target;
if (!(target instanceof HTMLElement)) {
throw new Error('Event target is not an HTMLElement.');
}
const { top, bottom, left, right } = target.getBoundingClientRect();
let clientX, clientY;
if (isTouchEvent(event)) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else {
clientX = event.clientX;
clientY = event.clientY;
}
// create a new point snapping to the edge of the current target element if it exceeds
// the bounding box of the target element
let x = Math.min(Math.max(left, clientX), right) - left;
let y = Math.min(Math.max(top, clientY), bottom) - top;
// adjust for DPI
x *= dpi;
y *= dpi;
return new Point(x, y);
}
}

View File

@ -0,0 +1,212 @@
'use client';
import {
HTMLAttributes,
MouseEvent,
PointerEvent,
TouchEvent,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { StrokeOptions, getStroke } from 'perfect-freehand';
import { cn } from '@documenso/ui/lib/utils';
import { getSvgPathFromStroke } from './helper';
import { Point } from './point';
const DPI = 2;
export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
onChange?: (_signatureDataUrl: string | null) => void;
};
export const SignaturePad = ({ className, onChange, ...props }: SignaturePadProps) => {
const $el = useRef<HTMLCanvasElement>(null);
const [isPressed, setIsPressed] = useState(false);
const [points, setPoints] = useState<Point[]>([]);
const perfectFreehandOptions = useMemo(() => {
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
return {
size,
thinning: 0.25,
streamline: 0.5,
smoothing: 0.5,
end: {
taper: size * 2,
},
} satisfies StrokeOptions;
}, []);
const onMouseDown = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
setIsPressed(true);
const point = Point.fromEvent(event, DPI, $el.current);
const newPoints = [...points, point];
setPoints(newPoints);
if ($el.current) {
const ctx = $el.current.getContext('2d');
if (ctx) {
ctx.save();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
const pathData = new Path2D(
getSvgPathFromStroke(getStroke(newPoints, perfectFreehandOptions)),
);
ctx.fill(pathData);
}
}
};
const onMouseMove = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
if (!isPressed) {
return;
}
const point = Point.fromEvent(event, DPI, $el.current);
if (point.distanceTo(points[points.length - 1]) > 5) {
const newPoints = [...points, point];
setPoints(newPoints);
if ($el.current) {
const ctx = $el.current.getContext('2d');
if (ctx) {
ctx.restore();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
const pathData = new Path2D(
getSvgPathFromStroke(getStroke(points, perfectFreehandOptions)),
);
ctx.fill(pathData);
}
}
}
};
const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addPoint = true) => {
if (event.cancelable) {
event.preventDefault();
}
setIsPressed(false);
const point = Point.fromEvent(event, DPI, $el.current);
const newPoints = [...points];
if (addPoint) {
newPoints.push(point);
setPoints(newPoints);
}
if ($el.current) {
const ctx = $el.current.getContext('2d');
if (ctx) {
ctx.restore();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
const pathData = new Path2D(
getSvgPathFromStroke(getStroke(newPoints, perfectFreehandOptions)),
);
ctx.fill(pathData);
ctx.save();
}
onChange?.($el.current.toDataURL());
}
setPoints([]);
};
const onMouseEnter = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
if ('buttons' in event && event.buttons === 1) {
onMouseDown(event);
}
};
const onMouseLeave = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
onMouseUp(event, false);
};
const onClearClick = () => {
if ($el.current) {
const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
}
onChange?.(null);
setPoints([]);
};
useEffect(() => {
if ($el.current) {
$el.current.width = $el.current.clientWidth * DPI;
$el.current.height = $el.current.clientHeight * DPI;
}
}, []);
return (
<div className="relative block">
<canvas
ref={$el}
className={cn('relative block', className)}
style={{ touchAction: 'none' }}
onPointerMove={(event) => onMouseMove(event)}
onPointerDown={(event) => onMouseDown(event)}
onPointerUp={(event) => onMouseUp(event)}
onPointerLeave={(event) => onMouseLeave(event)}
onPointerEnter={(event) => onMouseEnter(event)}
{...props}
/>
<div className="absolute bottom-2 right-2">
<button className="rounded-full p-2 text-xs text-slate-500" onClick={() => onClearClick()}>
Clear Signature
</button>
</div>
</div>
);
};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,28 @@
import { useState } from 'react';
export type CopiedValue = string | null;
export type CopyFn = (_text: string) => Promise<boolean>;
export function useCopyToClipboard(): [CopiedValue, CopyFn] {
const [copiedText, setCopiedText] = useState<CopiedValue>(null);
const copy: CopyFn = async (text) => {
if (!navigator?.clipboard) {
console.warn('Clipboard not supported');
return false;
}
// Try to save to clipboard then save it in the state if worked
try {
await navigator.clipboard.writeText(text);
setCopiedText(text);
return true;
} catch (error) {
console.warn('Copy failed', error);
setCopiedText(null);
return false;
}
};
return [copiedText, copy];
}

View File

@ -0,0 +1,128 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { randomUUID } from 'crypto';
import { hashSync } from '@documenso/lib/server-only/auth/hash';
import { redis } from '@documenso/lib/server-only/redis';
import { stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import { TClaimPlanResponseSchema, ZClaimPlanRequestSchema } from '~/api/claim-plan/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<TClaimPlanResponseSchema>,
) {
try {
const { method } = req;
if (method?.toUpperCase() !== 'POST') {
return res.status(405).json({
error: 'Method not allowed',
});
}
const safeBody = ZClaimPlanRequestSchema.safeParse(req.body);
if (!safeBody.success) {
return res.status(400).json({
error: 'Bad request',
});
}
const { email, name, planId, signatureDataUrl, signatureText } = safeBody.data;
const user = await prisma.user.findFirst({
where: {
email: email.toLowerCase(),
},
include: {
Subscription: true,
},
});
if (user && user.Subscription.length > 0) {
return res.status(200).json({
// eslint-disable-next-line turbo/no-undeclared-env-vars
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/login`,
});
}
const password = Math.random().toString(36).slice(2, 9);
const passwordHash = hashSync(password);
const { id: userId } = await prisma.user.upsert({
where: {
email: email.toLowerCase(),
},
create: {
email: email.toLowerCase(),
name,
password: passwordHash,
},
update: {
name,
password: passwordHash,
},
});
await redis.set(`user:${userId}:temp-password`, password, {
// expire in 24 hours
ex: 60 * 60 * 24,
});
const signatureDataUrlKey = randomUUID();
if (signatureDataUrl) {
await redis.set(`signature:${signatureDataUrlKey}`, signatureDataUrl, {
// expire in 7 days
ex: 60 * 60 * 24 * 7,
});
}
const metadata: Record<string, string> = {
name,
email,
signatureText: signatureText || name,
source: 'landing',
};
if (signatureDataUrl) {
metadata.signatureDataUrl = signatureDataUrlKey;
}
const checkout = await stripe.checkout.sessions.create({
customer_email: email,
client_reference_id: userId.toString(),
payment_method_types: ['card'],
line_items: [
{
price: planId,
quantity: 1,
},
],
mode: 'subscription',
metadata,
allow_promotion_codes: true,
// eslint-disable-next-line turbo/no-undeclared-env-vars
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?email=${encodeURIComponent(
email,
)}&name=${encodeURIComponent(name)}&planId=${planId}&cancelled=true`,
});
if (!checkout.url) {
throw new Error('Checkout URL not found');
}
return res.json({
redirectUrl: checkout.url,
});
} catch (error) {
console.error(error);
return res.status(500).json({
error: 'Internal server error',
});
}
}

View File

@ -0,0 +1,173 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { randomBytes } from 'crypto';
import { readFileSync } from 'fs';
import { buffer } from 'micro';
import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf';
import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf';
import { redis } from '@documenso/lib/server-only/redis';
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import {
DocumentStatus,
FieldType,
ReadStatus,
SendStatus,
SigningStatus,
} from '@documenso/prisma/client';
const log = (...args: any[]) => console.log('[stripe]', ...args);
export const config = {
api: { bodyParser: false },
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
// if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
// return res.status(500).json({
// success: false,
// message: 'Subscriptions are not enabled',
// });
// }
const sig =
typeof req.headers['stripe-signature'] === 'string' ? req.headers['stripe-signature'] : '';
if (!sig) {
return res.status(400).json({
success: false,
message: 'No signature found in request',
});
}
log('constructing body...');
const body = await buffer(req);
log('constructed body');
const event = stripe.webhooks.constructEvent(
body,
sig,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, turbo/no-undeclared-env-vars
process.env.NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET!,
);
log('event-type:', event.type);
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session;
if (session.metadata?.source === 'landing') {
const user = await prisma.user.findFirst({
where: {
id: Number(session.client_reference_id),
},
});
if (!user) {
return res.status(500).json({
success: false,
message: 'User not found',
});
}
const signatureText = session.metadata?.signatureText || user.name;
let signatureDataUrl = '';
if (session.metadata?.signatureDataUrl) {
const result = await redis.get<string>(`signature:${session.metadata.signatureDataUrl}`);
if (result) {
signatureDataUrl = result;
}
}
const now = new Date();
const document = await prisma.document.create({
data: {
title: 'Documenso Supporter Pledge.pdf',
status: DocumentStatus.COMPLETED,
userId: user.id,
document: readFileSync('./public/documenso-supporter-pledge.pdf').toString('base64'),
created: now,
},
});
const recipient = await prisma.recipient.create({
data: {
name: user.name ?? '',
email: user.email,
token: randomBytes(16).toString('hex'),
signedAt: now,
readStatus: ReadStatus.OPENED,
sendStatus: SendStatus.SENT,
signingStatus: SigningStatus.SIGNED,
documentId: document.id,
},
});
const field = await prisma.field.create({
data: {
documentId: document.id,
recipientId: recipient.id,
type: FieldType.SIGNATURE,
page: 0,
positionX: 77,
positionY: 638,
inserted: false,
customText: '',
},
});
if (signatureDataUrl) {
document.document = await insertImageInPDF(
document.document,
signatureDataUrl,
field.positionX,
field.positionY,
field.page,
);
} else {
document.document = await insertTextInPDF(
document.document,
signatureText ?? '',
field.positionX,
field.positionY,
field.page,
);
}
await Promise.all([
prisma.signature.create({
data: {
fieldId: field.id,
recipientId: recipient.id,
signatureImageAsBase64: signatureDataUrl || undefined,
typedSignature: signatureDataUrl ? '' : signatureText,
},
}),
prisma.document.update({
where: {
id: document.id,
},
data: {
document: document.document,
},
}),
]);
}
return res.status(200).json({
success: true,
message: 'Webhook received',
});
}
log('Unhandled webhook event', event.type);
return res.status(400).json({
success: false,
message: 'Unhandled webhook event',
});
}

View File

@ -0,0 +1,13 @@
'use client';
import React from 'react';
import NextPlausibleProvider from 'next-plausible';
export type PlausibleProviderProps = {
children: React.ReactNode;
};
export const PlausibleProvider = ({ children }: PlausibleProviderProps) => {
return <NextPlausibleProvider domain="documenso.com">{children}</NextPlausibleProvider>;
};

View File

@ -0,0 +1,11 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const baseConfig = require('@documenso/tailwind-config');
const path = require('path');
module.exports = {
...baseConfig,
content: [
...baseConfig.content,
`${path.join(require.resolve('@documenso/ui'), '..')}/**/*.{ts,tsx}`,
],
};

View File

@ -0,0 +1,26 @@
{
"extends": "@documenso/tsconfig/nextjs.json",
"compilerOptions": {
"allowJs": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"~/*": [
"./src/*"
]
},
"strictNullChecks": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

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

View File

@ -1,3 +0,0 @@
{
"extends": ["next/babel", "next/core-web-vitals"]
}

1
apps/web/README.md Normal file
View File

@ -0,0 +1 @@
# @documenso/web

View File

@ -1,88 +0,0 @@
import React, { useState } from "react";
import Draggable from "react-draggable";
import Logo from "../logo";
import { IconButton } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/20/solid";
const stc = require("string-to-color");
type FieldPropsType = {
field: {
color: string;
type: string;
position: any;
positionX: number;
positionY: number;
id: string;
Recipient: { name: ""; email: "" };
};
onPositionChanged: any;
onDelete: any;
hidden: boolean;
};
export default function EditableField(props: FieldPropsType) {
const [field, setField]: any = useState(props.field);
const [position, setPosition]: any = useState({
x: props.field.positionX,
y: props.field.positionY,
});
const nodeRef = React.createRef<HTMLDivElement>();
const onControlledDrag = (e: any, position: any) => {
const { x, y } = position;
setPosition({ x, y });
};
const onDragStop = (e: any, position: any) => {
if (!position) return;
const { x, y } = position;
props.onPositionChanged({ x, y }, props.field.id);
};
return (
<Draggable
nodeRef={nodeRef}
bounds="parent"
position={position}
onDrag={onControlledDrag}
onStop={onDragStop}
defaultPosition={{ x: 0, y: 0 }}
cancel="strong"
onMouseDown={(e: any) => {
e.preventDefault();
e.stopPropagation();
}}
>
{/* width: 192 height 96 */}
<div
hidden={props.hidden}
ref={nodeRef}
className="cursor-move opacity-80 p-2 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none"
style={{
background: stc(props.field.Recipient.email),
}}
>
<div className="m-auto overflow-hidden flex-row-reverse text-lg font-bold text-center">
{field.type}
{field.type === "SIGNATURE" ? (
<div className="text-xs text-center">
{`${props.field.Recipient?.name} <${props.field.Recipient?.email}>`}
</div>
) : (
""
)}
</div>
<strong>
<IconButton
className="absolute top-0 right-0 -m-5"
color="secondary"
icon={XCircleIcon}
onClick={(event: any) => {
props.onDelete(props.field.id);
}}
></IconButton>
</strong>
</div>
</Draggable>
);
}

View File

@ -1,78 +0,0 @@
import { useEffect, useState } from "react";
import { RadioGroup } from "@headlessui/react";
import { classNames } from "@documenso/lib";
import { FieldType } from "@prisma/client";
const stc = require("string-to-color");
export default function FieldTypeSelector(props: any) {
const fieldTypes = [
{
name: "Signature",
id: FieldType.SIGNATURE,
},
{ name: "Date", id: FieldType.DATE },
];
const [selectedFieldType, setSelectedFieldType] = useState(fieldTypes[0].id);
useEffect(() => {
props.onChange(selectedFieldType);
}, [selectedFieldType]);
return (
<RadioGroup
value={selectedFieldType}
onChange={(e: any) => {
setSelectedFieldType(e);
}}
onMouseDown={(e: any) => {
if (e.button === 0) props.setAdding(true);
}}
>
<div className="space-y-4">
{fieldTypes.map((fieldType) => (
<RadioGroup.Option
onMouseDown={(e: any) => {
if (e.button === 0) setSelectedFieldType(fieldType.id);
}}
key={fieldType.id}
value={fieldType.id}
className={({ checked, active }) =>
classNames(
checked ? "border-neon border-2" : "border-transparent",
"hover:bg-slate-100 select-none relative block cursor-pointer rounded-lg border bg-white px-3 py-2 focus:outline-none sm:flex sm:justify-between"
)
}
>
{({ active, checked }) => (
<>
<span className="flex items-center">
<span className="flex flex-col text-sm">
<RadioGroup.Label
as="span"
className="font-medium text-gray-900"
>
<span
className="inline-block h-4 w-4 flex-shrink-0 rounded-full mr-3 align-middle"
style={{
background: stc(props.selectedRecipient?.email),
}}
/>
<span className="align-middle">
{" "}
{
fieldTypes.filter((e) => e.id === fieldType.id)[0]
.name
}
</span>
</RadioGroup.Label>
</span>
</span>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
);
}

View File

@ -1,107 +0,0 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import { useState } from "react";
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
import { createField } from "@documenso/features/editor";
import RecipientSelector from "./recipient-selector";
import FieldTypeSelector from "./field-type-selector";
const stc = require("string-to-color");
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
ssr: false,
});
export default function PDFEditor(props: any) {
const router = useRouter();
const [fields, setFields] = useState<any[]>(props.document.Field);
const [selectedRecipient, setSelectedRecipient]: any = useState();
const [selectedFieldType, setSelectedFieldType] = useState();
const noRecipients = props?.document.Recipient.length === 0;
const [adding, setAdding] = useState(false);
function onPositionChangedHandler(position: any, id: any) {
if (!position) return;
const movedField = fields.find((e) => e.id == id);
movedField.positionX = position.x.toFixed(0);
movedField.positionY = position.y.toFixed(0);
createOrUpdateField(props.document, movedField);
// no instant redraw neccessary, postion information for saving or later rerender is enough
// setFields(newFields);
}
function onDeleteHandler(id: any) {
const field = fields.find((e) => e.id == id);
const fieldIndex = fields.map((item) => item.id).indexOf(id);
if (fieldIndex > -1) {
const fieldWithoutRemoved = [...fields];
const removedField = fieldWithoutRemoved.splice(fieldIndex, 1);
setFields(fieldWithoutRemoved);
deleteField(field).catch((err) => {
setFields(fieldWithoutRemoved.concat(removedField));
});
}
}
return (
<>
<div>
<PDFViewer
style={{
cursor: `url("https://place-hold.it/110x64/37f095/FFFFFF&text=${selectedFieldType}") 55 32, auto`,
}}
readonly={false}
document={props.document}
fields={fields}
onPositionChanged={onPositionChangedHandler}
onDelete={onDeleteHandler}
pdfUrl={`${NEXT_PUBLIC_WEBAPP_URL}/api/documents/${router.query.id}`}
onMouseUp={(e: any, page: number) => {
e.preventDefault();
e.stopPropagation();
console.log(adding);
if (adding) {
addField(e, page);
setAdding(false);
}
}}
onMouseDown={(e: any, page: number) => {
if (e.button === 0) addField(e, page);
}}
></PDFViewer>
<div
hidden={noRecipients}
className="fixed left-0 top-1/3 max-w-xs border border-slate-300 bg-white py-4 pr-5 rounded-md"
>
<RecipientSelector
recipients={props?.document?.Recipient}
onChange={setSelectedRecipient}
/>
<hr className="m-3 border-slate-300"></hr>
<FieldTypeSelector
setAdding={setAdding}
selectedRecipient={selectedRecipient}
onChange={setSelectedFieldType}
/>
</div>
</div>
</>
);
function addField(e: any, page: number) {
if (!selectedRecipient) return;
if (!selectedFieldType) return;
const signatureField = createField(
e,
page,
selectedRecipient,
selectedFieldType
);
createOrUpdateField(props?.document, signatureField).then((res) => {
setFields(fields.concat(res));
});
}
}

View File

@ -1,209 +0,0 @@
import Logo from "../logo";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import SignatureDialog from "./signature-dialog";
import { useEffect, useState } from "react";
import { Button } from "@documenso/ui";
import {
CheckBadgeIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import toast from "react-hot-toast";
import { FieldType } from "@prisma/client";
import {
createOrUpdateField,
deleteField,
signDocument,
} from "@documenso/lib/api";
import { createField } from "@documenso/features/editor";
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
ssr: false,
});
export default function PDFSigner(props: any) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [signingDone, setSigningDone] = useState(false);
const [localSignatures, setLocalSignatures] = useState<any[]>([]);
const [fields, setFields] = useState<any[]>(props.fields);
const signatureFields = fields.filter(
(field) => field.type === FieldType.SIGNATURE
);
const [dialogField, setDialogField] = useState<any>();
useEffect(() => {
setSigningDone(checkIfSigningIsDone());
}, [fields]);
function onClick(item: any) {
if (item.type === "SIGNATURE") {
setDialogField(item);
setOpen(true);
}
}
function onDialogClose(dialogResult: any) {
// todo handle signature removed from field, remove free field if dialogresult is empty (or the id )
if (!dialogResult && dialogField.type === "FREE_SIGNATURE") {
onDeleteHandler(dialogField.id);
return;
}
if (!dialogResult) return;
const signature = {
fieldId: dialogField.id,
type: dialogResult.type,
typedSignature: dialogResult.typedSignature,
signatureImage: dialogResult.signatureImage,
};
setLocalSignatures(localSignatures.concat(signature));
fields.splice(
fields.findIndex(function (i) {
return i.id === signature.fieldId;
}),
1
);
const signedField = { ...dialogField };
signedField.signature = signature;
setFields(fields.concat(signedField));
setOpen(false);
setDialogField(null);
}
return (
<>
<SignatureDialog open={open} setOpen={setOpen} onClose={onDialogClose} />
<div className="bg-neon p-4">
<div className="flex">
<div className="flex-shrink-0">
<Logo className="h-12 w-12 -mt-2.5"></Logo>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between text-center justify-start items-center">
<p className="text-lg text-slate-700">
{props.document.User.name
? `${props.document.User.name} (${props.document.User.email})`
: props.document.User.email}{" "}
would like you to sign this document.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<Button
disabled={!signingDone}
color="secondary"
icon={CheckBadgeIcon}
className="float-right"
onClick={() => {
signDocument(
props.document,
localSignatures,
`${router.query.token}`
).then(() => {
router.push(
`/documents/${props.document.id}/signed?token=${router.query.token}`
);
});
}}
>
Done
</Button>
</p>
</div>
</div>
</div>
{signatureFields.length === 0 ? (
<div className="bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-yellow-700">
You can sign this document anywhere you like, but maybe look for
a signature line.
</p>
</div>
</div>
</div>
) : null}
<PDFViewer
style={{
cursor:
signatureFields.length === 0
? `url("https://place-hold.it/110x64/37f095/ffffff&text=Signature") 55 32, auto`
: "",
}}
readonly={true}
document={props.document}
fields={fields}
pdfUrl={`${NEXT_PUBLIC_WEBAPP_URL}/api/documents/${router.query.id}?token=${router.query.token}`}
onClick={onClick}
onMouseDown={function onMouseDown(e: any, page: number) {
if (signatureFields.length === 0)
addFreeSignature(e, page, props.recipient);
}}
onMouseUp={() => {}}
onDelete={onDeleteHandler}
></PDFViewer>
</>
);
function checkIfSigningIsDone(): boolean {
// Check if all fields are signed..
if (signatureFields.length > 0) {
// If there are no fields to sign at least one signature is enough
return fields
.filter((field) => field.type === FieldType.SIGNATURE)
.every((field) => field.signature);
} else {
return localSignatures.length > 0;
}
}
function addFreeSignature(e: any, page: number, recipient: any): any {
const freeSignatureField = createField(
e,
page,
recipient,
FieldType.FREE_SIGNATURE
);
createOrUpdateField(props.document, freeSignatureField).then((res) => {
setFields(fields.concat(res));
setDialogField(res);
setOpen(true);
});
return freeSignatureField;
}
function onDeleteHandler(id: any) {
const field = fields.find((e) => e.id == id);
const fieldIndex = fields.map((item) => item.id).indexOf(id);
if (fieldIndex > -1) {
const fieldWithoutRemoved = [...fields];
const removedField = fieldWithoutRemoved.splice(fieldIndex, 1);
setFields(fieldWithoutRemoved);
const signaturesWithoutRemoved = [...localSignatures];
const removedSignature = signaturesWithoutRemoved.splice(
signaturesWithoutRemoved.findIndex(function (i) {
return i.fieldId === id;
}),
1
);
setLocalSignatures(signaturesWithoutRemoved);
deleteField(field).catch((err) => {
setFields(fieldWithoutRemoved.concat(removedField));
setLocalSignatures(signaturesWithoutRemoved.concat(removedSignature));
});
}
}
}

View File

@ -1,169 +0,0 @@
import { Fragment, useState } from "react";
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
import EditableField from "./editable-field";
import SignableField from "./signable-field";
import short from "short-uuid";
export default function PDFViewer(props) {
const [numPages, setNumPages] = useState(null);
const [loading, setLoading] = useState(true);
const [pageHeight, setPageHeight] = useState(0);
function onPositionChangedHandler(position, id) {
props.onPositionChanged(position, id);
}
function onDeleteHandler(id) {
props.onDelete(id);
}
function onDocumentLoadSuccess({ numPages: nextNumPages }) {
setNumPages(nextNumPages);
}
const options = {
cMapUrl: "cmaps/",
cMapPacked: true,
standardFontDataUrl: "standard_fonts/",
};
return (
<>
<div
hidden={loading}
onMouseUp={props.onMouseUp}
style={{ height: numPages * pageHeight + 1000 }}
>
<div className="max-w-xs mt-6"></div>
<Document
file={props.pdfUrl}
onLoadSuccess={onDocumentLoadSuccess}
options={options}
renderMode="canvas"
className="absolute w-auto mx-auto left-0 right-0"
>
{Array.from({ length: numPages }, (_, index) => (
<Fragment key={short.generate().toString()}>
<div
onMouseDown={(e) => {
if (e.button === 0) props.onMouseDown(e, index);
}}
onMouseUp={(e) => {
if (e.button === 0) props.onMouseUp(e, index);
}}
key={short.generate().toString()}
style={{
position: "relative",
...props.style,
}}
className="mx-auto w-fit"
>
<Page
className="mt-5"
key={`page_${index + 1}`}
pageNumber={index + 1}
renderAnnotationLayer={false}
renderTextLayer={false}
onLoadSuccess={(e) => {
if (e.height) setPageHeight(e.height);
setLoading(false);
}}
onRenderError={() => setLoading(false)}
></Page>
{props?.fields
.filter((item) => item.page === index)
.map((item) =>
props.readonly ? (
<SignableField
onClick={props.onClick}
key={item.id}
field={item}
className="absolute"
onDelete={onDeleteHandler}
></SignableField>
) : (
<EditableField
hidden={item.Signature || item.inserted}
key={item.id}
field={item}
className="absolute"
onPositionChanged={onPositionChangedHandler}
onDelete={onDeleteHandler}
></EditableField>
)
)}
</div>
</Fragment>
))}
</Document>
</div>
<div className="mx-auto mt-10 w-[600px]" hidden={!loading}>
<div className="ph-item">
<div className="ph-col-12">
<div className="ph-picture"></div>
<div className="ph-row">
<div className="ph-col-6 big"></div>
<div className="ph-col-4 empty big"></div>
<div className="ph-col-2 big"></div>
<div className="ph-col-4"></div>
<div className="ph-col-8 empty"></div>
<div className="ph-col-6"></div>
<div className="ph-col-6 empty"></div>
<div className="ph-col-12"></div>
</div>
<div className="ph-row">
<div className="ph-col-6 big"></div>
<div className="ph-col-4 empty big"></div>
<div className="ph-col-2 big"></div>
<div className="ph-col-4"></div>
<div className="ph-col-8 empty"></div>
<div className="ph-col-6"></div>
<div className="ph-col-6 empty"></div>
<div className="ph-col-12"></div>
</div>
<div className="ph-row">
<div className="ph-col-6 big"></div>
<div className="ph-col-4 empty big"></div>
<div className="ph-col-2 big"></div>
<div className="ph-col-4"></div>
<div className="ph-col-8 empty"></div>
<div className="ph-col-6"></div>
<div className="ph-col-6 empty"></div>
<div className="ph-col-12"></div>
</div>
<div className="ph-row">
<div className="ph-col-6 big"></div>
<div className="ph-col-4 empty big"></div>
<div className="ph-col-2 big"></div>
<div className="ph-col-4"></div>
<div className="ph-col-8 empty"></div>
<div className="ph-col-6"></div>
<div className="ph-col-6 empty"></div>
<div className="ph-col-12"></div>
</div>
<div className="ph-row">
<div className="ph-col-6 big"></div>
<div className="ph-col-4 empty big"></div>
<div className="ph-col-2 big"></div>
<div className="ph-col-4"></div>
<div className="ph-col-8 empty"></div>
<div className="ph-col-6"></div>
<div className="ph-col-6 empty"></div>
<div className="ph-col-12"></div>
</div>
<div className="ph-row">
<div className="ph-col-6 big"></div>
<div className="ph-col-4 empty big"></div>
<div className="ph-col-2 big"></div>
<div className="ph-col-4"></div>
<div className="ph-col-8 empty"></div>
<div className="ph-col-6"></div>
<div className="ph-col-6 empty"></div>
<div className="ph-col-12"></div>
</div>
</div>
</div>
</div>
</>
);
}

View File

@ -1,101 +0,0 @@
import { Fragment, useEffect, useState } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
import { classNames } from "@documenso/lib";
const stc = require("string-to-color");
export default function RecipientSelector(props: any) {
const [selectedRecipient, setSelectedRecipient]: any = useState(
props?.recipients[0]
);
useEffect(() => {
props.onChange(selectedRecipient);
}, [selectedRecipient]);
return (
<Listbox
value={selectedRecipient}
onChange={(e: any) => {
setSelectedRecipient(e);
}}
>
{({ open }) => (
<div className="relative mt-1 mb-2">
<Listbox.Button className="select-none relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:border-neon focus:outline-none focus:ring-1 focus:ring-neon sm:text-sm">
<span className="flex items-center">
<span
className="inline-block h-4 w-4 flex-shrink-0 rounded-full"
style={{ background: stc(selectedRecipient?.email) }}
/>
<span className="ml-3 block truncate">
{`${selectedRecipient?.name} <${selectedRecipient?.email}>`}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{props?.recipients.map((recipient: any) => (
<Listbox.Option
key={recipient?.id}
className={({ active }) =>
classNames(
active ? "text-white bg-neon-dark" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9"
)
}
value={recipient}
>
{({ selected, active }) => (
<>
<div className="flex items-center">
<span
className="inline-block h-4 w-4 flex-shrink-0 rounded-full"
style={{
background: stc(recipient?.email),
}}
/>
<span
className={classNames(
selected ? "font-semibold" : "font-normal",
"ml-3 block truncate"
)}
>
{`${selectedRecipient?.name} <${selectedRecipient?.email}>`}
</span>
</div>
{selected ? (
<span
className={classNames(
active ? "text-white" : "text-neon-dark",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
);
}

View File

@ -1,87 +0,0 @@
import React, { useState } from "react";
import Draggable from "react-draggable";
import { IconButton } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/20/solid";
const stc = require("string-to-color");
type FieldPropsType = {
field: {
color: string;
type: string;
position: any;
positionX: number;
positionY: number;
id: string;
Recipient: { name: ""; email: "" };
};
onClick: any;
onDelete: any;
};
export default function SignableField(props: FieldPropsType) {
const [field, setField]: any = useState(props.field);
const [position, setPosition]: any = useState({
x: props.field.positionX,
y: props.field.positionY,
});
const nodeRef = React.createRef<HTMLDivElement>();
return (
<Draggable
nodeRef={nodeRef}
bounds="parent"
position={position}
defaultPosition={{ x: 0, y: 0 }}
cancel="div"
onMouseDown={(e: any) => {
e.preventDefault();
e.stopPropagation();
}}
>
<div
onClick={() => {
if (!field?.signature) props.onClick(props.field);
}}
ref={nodeRef}
className="cursor-pointer opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none hover:brightness-50"
style={{
background: stc(props.field.Recipient.email),
}}
>
<div hidden={field?.signature} className="font-medium my-4">
{field.type === "SIGNATURE" ? "SIGN HERE" : ""}
</div>
<div
hidden={!field?.signature}
className="font-qwigley text-5xl m-auto w-auto flex-row-reverse font-medium text-center"
>
{field?.signature?.type === "type" ? (
<div className="my-4">{field?.signature.typedSignature}</div>
) : (
""
)}
{field?.signature?.type === "draw" ? (
<img className="w-48 h-16" src={field?.signature?.signatureImage} />
) : (
""
)}
<IconButton
icon={XCircleIcon}
color="secondary"
className="absolute top-0 right-0 -m-5"
onClick={(event: any) => {
event.preventDefault();
event.stopPropagation();
const newField = { ...field };
newField.signature = null;
setField(newField);
// remove not only signature but whole field if it is a freely places signature
if (field.type === "FREE_SIGNATURE") props.onDelete(field.id);
}}
/>
</div>
</div>
</Draggable>
);
}

View File

@ -1,213 +0,0 @@
import { classNames } from "@documenso/lib";
import { Button, IconButton } from "@documenso/ui";
import { Dialog, Transition } from "@headlessui/react";
import {
LanguageIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useEffect, useState } from "react";
import SignatureCanvas from "react-signature-canvas";
import { localStorage } from "@documenso/lib";
const tabs = [
{ name: "Type", icon: LanguageIcon, current: true },
{ name: "Draw", icon: PencilIcon, current: false },
];
export default function SignatureDialog(props: any) {
const [currentTab, setCurrentTab] = useState(tabs[0]);
const [typedSignature, setTypedSignature] = useState("");
const [signatureEmpty, setSignatureEmpty] = useState(true);
let signCanvasRef: any | undefined;
useEffect(() => {
setTypedSignature(localStorage.getItem("typedSignature") || "");
}, []);
return (
<>
<Transition.Root show={props.open} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={() => {
props.setOpen(false);
setCurrent(tabs[0]);
}}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="min-h-[350px] relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div className="">
<div className="border-b border-gray-200 mb-3">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<a
key={tab.name}
onClick={() => {
setCurrent(tab);
}}
className={classNames(
tab.current
? "border-neon text-neon"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
"group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm cursor-pointer"
)}
aria-current={tab.current ? "page" : undefined}
>
<tab.icon
className={classNames(
tab.current
? "text-neon"
: "text-gray-400 group-hover:text-gray-500",
"-ml-0.5 mr-2 h-5 w-5"
)}
aria-hidden="true"
/>
<span>{tab.name}</span>
</a>
))}
</nav>
</div>
{isCurrentTab("Type") ? (
<div>
<div className="my-8 border-b border-gray-300 mb-3">
<input
value={typedSignature}
onChange={(e) => {
setTypedSignature(e.target.value);
}}
className={classNames(
typedSignature ? "font-qwigley text-4xl" : "",
"leading-none h-10 align-bottom mt-14 text-center block w-full focus:border-neon focus:ring-neon text-2xl"
)}
placeholder="Kindly type your name"
/>
</div>
<div className="float-right">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}
>
Cancel
</Button>
<Button
className="ml-3"
disabled={!typedSignature}
onClick={() => {
localStorage.setItem(
"typedSignature",
typedSignature
);
props.onClose({
type: "type",
typedSignature: typedSignature,
});
}}
>
Sign
</Button>
</div>
</div>
) : (
""
)}
{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="block float-left"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
></IconButton>
<div className="mt-10 float-right">
<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>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</>
);
function isCurrentTab(tabName: string): boolean {
return currentTab.name === tabName;
}
function setCurrent(t: any) {
tabs.forEach((tab) => {
tab.current = tab.name === t.name;
});
setCurrentTab(t);
}
}

View File

@ -1,44 +0,0 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import Navigation from "./navigation";
function useRedirectToLoginIfUnauthenticated() {
const { data: session, status } = useSession();
const loading = status === "loading";
const router = useRouter();
useEffect(() => {
if (!loading && !session) {
router.replace({
pathname: "/login",
query: {
callbackUrl: `${NEXT_PUBLIC_WEBAPP_URL}/${location.pathname}${location.search}`,
},
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loading, session]);
return {
loading: loading && !session,
session,
};
}
export default function Layout({ children }: any) {
useRedirectToLoginIfUnauthenticated();
return (
<>
<div className="min-h-full">
<Navigation></Navigation>
<main>
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">{children}</div>
</main>
</div>
</>
);
}

View File

@ -1,171 +0,0 @@
import { LockClosedIcon } from "@heroicons/react/20/solid";
import Link from "next/link";
import { FormProvider, useForm } from "react-hook-form";
import Logo from "./logo";
import { signIn } from "next-auth/react";
import { useState } from "react";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
interface LoginValues {
email: string;
password: string;
totpCode: string;
csrfToken: string;
}
export default function Login() {
const router = useRouter();
const methods = useForm<LoginValues>();
const { register, formState } = methods;
const [errorMessage, setErrorMessage] = useState<string | null>(null);
let callbackUrl =
typeof router.query?.callbackUrl === "string"
? router.query.callbackUrl
: "";
// If not absolute URL, make it absolute
if (!/^https?:\/\//.test(callbackUrl)) {
callbackUrl = `${NEXT_PUBLIC_WEBAPP_URL}/${callbackUrl}`;
}
const onSubmit = async (values: LoginValues) => {
setErrorMessage(null);
const res = await toast.promise(
signIn<"credentials">("credentials", {
...values,
callbackUrl,
redirect: false,
}),
{
loading: "Logging in...",
success: "Login successful.",
error: "Could not log in :/",
},
{
style: {
minWidth: "200px",
},
}
);
if (!res) {
setErrorMessage("Error");
toast.dismiss();
toast.error("Something went wrong.");
} else if (!res.error) {
// we're logged in, let's do a hard refresh to the original url
router.push(callbackUrl);
} else {
toast.dismiss();
if (res.status == 401) {
toast.error("Invalid email or password.");
} else {
toast.error("Could not login.");
}
}
};
return (
<>
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
<div>
<Logo className="mx-auto h-10 w-auto"></Logo>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Sign in to your account
</h2>
</div>
<FormProvider {...methods}>
<form
className="mt-8 space-y-6"
onSubmit={methods.handleSubmit(onSubmit)}
>
<input type="hidden" name="remember" defaultValue="true" />
<div className="-space-y-px rounded-md shadow-sm">
<div>
<label htmlFor="email-address" className="sr-only">
Email
</label>
<input
{...register("email")}
id="email-address"
name="email"
type="email"
autoComplete="email"
required
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
placeholder="Email"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
{...register("password")}
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<a href="#" className="font-medium text-neon hover:text-neon">
Forgot your password?
</a>
</div>
</div>
<div>
<Button
type="submit"
disabled={formState.isSubmitting}
className="group relative flex w-full"
>
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<LockClosedIcon
className="h-5 w-5 text-neon-dark group-hover:text-neon disabled:group-hover:bg-gray-600 disabled:disabled:bg-gray-600"
aria-hidden="true"
/>
</span>
Sign in
</Button>
</div>
<div>
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center"></div>
</div>
</div>
<p className="mt-2 text-center text-sm text-gray-600">
Are you new here?{" "}
<Link
href="/signup"
className="font-medium text-neon hover:text-neon"
>
Create a new Account
</Link>
</p>
</form>
</FormProvider>
</div>
</div>
{/* <Toaster position="top-center" /> */}
</>
);
}

View File

@ -1,28 +0,0 @@
import { classNames } from "@documenso/lib";
import Link from "next/link";
export default function Logo(props: any) {
return (
<>
<Link href="/dashboard">
<svg
className="w-12"
viewBox="0 0 88.6758041381836 32.18000030517578"
{...props}
>
<rect
width="88.6758041381836"
height="32.18000030517578"
fill="transparent"
></rect>
<g transform="matrix(1,0,0,1,-25.98720359802246,-66.41200256347656)">
<path
d="M99.004,68.26l-.845-1.848-23.226,2.299c-1.437,.142-2.876,.089-4.3-.156-2.949-.51-6.32-1.717-9.073-2.099-1.477-.206-2.934,.448-3.789,1.67-1.174,1.678-2.308,3.888-3.501,5.622-1.518,2.207-3.032,4.418-4.531,6.638-1.693,2.499-3.365,5.013-5.061,7.51-.103,.153-.333,.221-.503,.329-.079-.279-.306-.636-.206-.824,1.006-1.928,1.845-4,3.165-5.694,1.908-2.449,2.914-5.445,5.007-7.745,.342-.777,.845-1.466,1.151-2.244,.915-2.341-1.295-5.305-2.935-5.305h-.613c-1.196,0-2.526,.357-4.092,1.716-.986,.856-2.391,2.432-2.97,3.326-3.424,5.286-7.177,10.382-10.15,15.932-.365,.682-1.719,3.722-2.013,3.606-.214-.077-.159-.458-.041-.823,1.312-4.065,4.163-9.851,5.843-13.777,.41-.962,.635-1.516-.305-2.361-1.016-.913-2.669,.084-3.084,1.052-1.784,4.174-2.631,8.08-4.038,12.396-.466,1.43-.916,2.865-1.386,4.294-.273,.831-.548,1.661-.836,2.488-.112,.321-.226,.642-.345,.962-.032,.082-.064,.165-.095,.248-.006,.011-.003,.002-.009,.017-.005,.008-.003,.015-.006,.023-.011,.026-.021,.05-.03,.076-.024,.067-.012,.044,.009-.002-.691,1.577,.417,3.002,2.091,3.002,.808,0,1.552-1.431,1.943-2.055,1.224-1.957,3.28-5.125,4.36-7.163,.894-1.69,2.078-3.14,3.209-4.615,1.857-2.42,3.065-5.242,5.025-7.575,.33-.394,.694-.772,1.093-1.093,.121-.098,.473-.05,.618,.062,.124,.098,.2,.432,.127,.577-.397,.788-.863,1.542-1.278,2.322-1.481,2.79-2.953,5.582-4.425,8.377-1.16,2.204-2.659,5.055-3.551,6.755-.438,.833-.176,1.857,.604,2.382,0,0-.383,2.028,1.908,2.028,1.896,0,2.711-1.145,3.053-1.624,.348-.486,.748-.951,1.202-1.334,.151-.13,.563-.018,.821,.079,.076,.029,.085,.406,.03,.582-.398,1.272-.323,2.283,1.879,2.283,.784,0,2.218-1.283,2.904-2.213,.794-1.075,.731-1.1,1.415-2.244,1.678-2.821,3.132-5.055,4.751-7.91,1.083-1.91,2.175-3.854,3.294-5.516,.685-1.015,1.446-1.252,2.188-1.252s.409,.686,.336,1.025c-.071,.341-.677,1.545-2.531,4.35-1.115,1.687-2.244,3.367-3.308,5.084-1.075,1.736-2.141,3.48-3.205,5.225-.88,1.445,.162,3.467,1.854,3.467h2.765c2.653,0,5.302-.397,7.916-.851l7.41-1.287c1.421-.245,2.868-.298,4.303-.158l23.161,2.296,.845-1.849c4.61-9.858,15.211-11.322,15.66-11.38v-5.717c-.448-.058-11.05-1.522-15.66-11.381Zm-40.143,6.165c-.164,.465-.491,.922-.86,1.255-.918,.828-1.451,1.842-1.911,2.977-.512,1.269-1.829,2.119-1.966,3.65-.027,.3-.627,.577-1.006,.797-.109,.062-.357-.114-.524-.174,.015-.229-.024-.401,.039-.515,.848-1.495,1.678-3.002,2.584-4.462,.775-1.254,1.642-2.453,2.469-3.677,.085-.126,.158-.273,.27-.365,.457-.368,.93-.719,1.399-1.075,.497,.721-.312,1.071-.494,1.589Zm30.047,12.011c1.736,0,3.162-1.137,3.686-2.693h10.547c-3.02,1.916-6.1,4.684-8.402,8.716l-21.902-2.17v-15.584l21.902-2.167c2.302,4.032,5.382,6.802,8.402,8.714h-10.547c-.524-1.554-1.951-2.691-3.686-2.691-2.172,0-3.938,1.763-3.938,3.938s1.766,3.938,3.938,3.938Z"
className={classNames(props.dark ? "fill-white" : "fill-brown")}
></path>
</g>
</svg>
</Link>
</>
);
}

View File

@ -1,267 +0,0 @@
import { Fragment, useEffect, useState } from "react";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { signOut, useSession } from "next-auth/react";
import avatarFromInitials from "avatar-from-initials";
import { toast } from "react-hot-toast";
import {
Bars3Icon,
BellIcon,
XMarkIcon,
UserCircleIcon,
ArrowRightOnRectangleIcon,
DocumentTextIcon,
ChartBarIcon,
WrenchIcon,
} from "@heroicons/react/24/outline";
import Logo from "./logo";
import { getUser } from "@documenso/lib/api";
const navigation = [
{
name: "Dashboard",
href: "/dashboard",
current: false,
icon: ChartBarIcon,
},
{
name: "Documents",
href: "/documents",
current: false,
icon: DocumentTextIcon,
},
{
name: "Settings",
href: "/settings",
current: true,
icon: WrenchIcon,
},
];
const userNavigation = [
{ name: "Your Profile", href: "/settings/profile", icon: UserCircleIcon },
{
name: "Sign out",
href: "",
click: async (e: any) => {
e.preventDefault();
const res: any = await toast.promise(
signOut({ callbackUrl: "/login" }),
{
loading: "Logging out...",
success: "Your are logged out.",
error: "Could not log out :/",
},
{
style: {
minWidth: "200px",
},
success: {
duration: 10000,
},
}
);
},
icon: ArrowRightOnRectangleIcon,
},
];
function classNames(...classes: any) {
return classes.filter(Boolean).join(" ");
}
type UserType = {
id?: number | undefined;
name?: string | null;
email?: string | null;
image?: string | null;
};
export default function TopNavigation() {
const router = useRouter();
const session = useSession();
const [user, setUser] = useState({
email: "",
name: "",
});
useEffect(() => {
getUser().then((res) => {
res.json().then((j: any) => {
setUser(j);
});
});
}, [session]);
navigation.forEach((element) => {
element.current = router.route.endsWith("/" + element.href.split("/")[1]);
});
return (
<>
<Disclosure as="nav" className="border-b border-gray-200 bg-white">
{({ open }) => (
<>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 justify-between">
<div className="flex">
<div className="flex flex-shrink-0 items-center">
<Logo></Logo>
</div>
<div className="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8">
{navigation.map((item) => (
<Link
key={item.name}
href={item.href}
className={classNames(
item.current
? "border-neon text-brown"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
<item.icon
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
aria-hidden="true"
></item.icon>
{item.name}
</Link>
))}
</div>
</div>
<div
onClick={() => {
document?.getElementById("mb")?.click();
}}
className="hidden sm:ml-6 sm:flex sm:items-center hover:bg-gray-200 px-3 cursor-pointer"
>
<span className="text-sm">
<p className="font-bold">{user?.name || ""}</p>
<p>{user?.email}</p>
</span>
<Menu as="div" className="relative ml-3">
<div>
<Menu.Button
id="mb"
className="flex max-w-xs items-center rounded-full bg-white text-sm"
>
<span className="sr-only">Open user menu</span>
<div
key={user?.email}
dangerouslySetInnerHTML={{
__html: avatarFromInitials(
user?.name || "" || "",
40
),
}}
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
{({ active }) => (
<Link
href={item.href}
onClick={item.click}
className={classNames(
active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm text-gray-700"
)}
>
<item.icon
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
aria-hidden="true"
></item.icon>
{item.name}
</Link>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
</div>
<div className="-mr-2 flex items-center sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500">
<span className="sr-only">Open main menu</span>
{open ? (
<XMarkIcon className="block h-6 w-6" aria-hidden="true" />
) : (
<Bars3Icon className="block h-6 w-6" aria-hidden="true" />
)}
</Disclosure.Button>
</div>
</div>
</div>
<Disclosure.Panel className="sm:hidden">
<div className="space-y-1 pt-2 pb-3">
{navigation.map((item) => (
<Link
key={item.name}
href={item.href}
className={classNames(
item.current
? "bg-teal-50 border-teal-500 text-teal-700"
: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800",
"block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
{item.name}
</Link>
))}
</div>
<div className="border-t border-gray-200 pt-4 pb-3">
<div className="flex items-center px-4">
<div className="flex-shrink-0">
<div
key={user?.email}
dangerouslySetInnerHTML={{
__html: avatarFromInitials(user?.name || "" || "", 40),
}}
/>
</div>
<div className="ml-3">
<div className="text-base font-medium text-gray-800">
{user?.name || ""}
</div>
<div className="text-sm font-medium text-gray-500">
{user?.email}
</div>
</div>
</div>
<div className="mt-3 space-y-1">
{userNavigation.map((item) => (
<Link
key={item.name}
onClick={item.click}
href={item.href}
className="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800"
>
{item.name}
</Link>
))}
</div>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
{/* <Toaster position="top-center"></Toaster> */}
</>
);
}

View File

@ -1,195 +0,0 @@
import { ChangeEvent, useEffect, useState } from "react";
import { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import Link from "next/link";
import Head from "next/head";
import { useSession } from "next-auth/react";
import { updateUser } from "@documenso/features";
import { Button } from "@documenso/ui";
import { getUser } from "@documenso/lib/api";
const subNavigation = [
{
name: "Profile",
href: "/settings/profile",
icon: UserCircleIcon,
current: true,
},
{
name: "Password",
href: "/settings/password",
icon: KeyIcon,
current: false,
},
];
function classNames(...classes: any) {
return classes.filter(Boolean).join(" ");
}
export default function Setttings() {
const session = useSession();
const [user, setUser] = useState({
email: "",
name: "",
});
useEffect(() => {
getUser().then((res: any) => {
res.json().then((j: any) => {
setUser(j);
});
});
}, [session]);
const router = useRouter();
subNavigation.forEach((element) => {
element.current = element.href == router.route;
});
const [savingTimeout, setSavingTimeout] = useState<any>();
function handleNameChange(e: ChangeEvent<HTMLInputElement>): void {
let u = { ...user };
u.name = e.target.value;
setUser(u);
clearTimeout(savingTimeout);
const t = setTimeout(() => {
updateUser(u);
}, 1000);
setSavingTimeout(t);
}
const handleKeyPress = (event: any) => {
if (event.key === "Enter") {
clearTimeout(savingTimeout);
updateUser(user);
}
};
return (
<div>
<Head>
<title>Settings | Documenso</title>
</Head>
<header className="py-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold leading-tight tracking-tight text-brown">
Settings
</h1>
</div>
</header>
<div
className="mx-auto max-w-screen-xl px-4 pb-6 sm:px-6 lg:px-8 lg:pb-16"
hidden={!user.email}
>
<div className="overflow-hidden rounded-lg bg-white shadow">
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<aside className="py-6 lg:col-span-3">
<nav className="space-y-1">
{subNavigation.map((item) => (
<Link
key={item.name}
href={item.href}
className={classNames(
item.current
? "bg-teal-50 border-neon-dark text-teal-700 hover:bg-teal-50 hover:text-teal-700"
: "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900",
"group border-l-4 px-3 py-2 flex items-center text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
<item.icon
className={classNames(
item.current
? "text-teal-500 group-hover:text-teal-500"
: "text-gray-400 group-hover:text-gray-500",
"flex-shrink-0 -ml-1 mr-3 h-6 w-6"
)}
aria-hidden="true"
/>
<span className="truncate">{item.name}</span>
</Link>
))}
</nav>
</aside>
<form
className="divide-y divide-gray-200 lg:col-span-9"
action="#"
method="POST"
>
{/* Profile section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div>
<h2 className="text-lg font-medium leading-6 text-gray-900">
Profile
</h2>
<p className="mt-1 text-sm text-gray-500">
Let people know who they are dealing with builds trust.
</p>
</div>
<div className="my-6 grid grid-cols-12 gap-6">
<div className="col-span-12 sm:col-span-6">
<label
htmlFor="first-name"
className="block text-sm font-medium text-gray-700"
>
Full Name
</label>
<input
type="text"
name="first-name"
id="first-name"
value={user?.name || ""}
onChange={(e) => handleNameChange(e)}
onKeyDown={handleKeyPress}
autoComplete="given-name"
className="mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
/>
</div>
<div className="col-span-12 sm:col-span-6">
<label
htmlFor="first-name"
className="block text-sm font-medium text-gray-700"
>
Email
</label>
<input
disabled
value={user?.email!}
type="text"
name="first-name"
id="first-name"
autoComplete="given-name"
className="mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
/>
</div>
</div>
<Button onClick={() => updateUser(user)}>Save</Button>
</div>
</form>
</div>
</div>
</div>
<div className="mt-10 max-w-[1100px]" hidden={!!user.email}>
<div className="ph-item">
<div className="ph-col-12">
<div className="ph-picture"></div>
<div className="ph-row">
<div className="ph-col-6 big"></div>
<div className="ph-col-4 empty big"></div>
<div className="ph-col-2 big"></div>
<div className="ph-col-4"></div>
<div className="ph-col-8 empty"></div>
<div className="ph-col-6"></div>
<div className="ph-col-6 empty"></div>
<div className="ph-col-12"></div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,210 +0,0 @@
import { signup } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/24/outline";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
type FormValues = {
email: string;
password: string;
apiError: string;
};
export default function Signup(props: { source: string }) {
const form = useForm<FormValues>({});
const {
register,
trigger,
formState: { errors, isSubmitting },
} = form;
const handleErrors = async (resp: Response) => {
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.message);
}
};
const signUp: SubmitHandler<FormValues> = async (data) => {
await toast
.promise(
signup(props.source, data)
.then(handleErrors)
.then(async () => {
await signIn<"credentials">("credentials", {
...data,
callbackUrl: `${NEXT_PUBLIC_WEBAPP_URL}/dashboard`,
});
}),
{
loading: "Creating your account...",
success: "Done!",
error: (err) => err.message,
},
{
style: {
minWidth: "200px",
},
}
)
.catch((err) => {
toast.dismiss();
form.setError("apiError", { message: err.message });
});
};
function renderApiError() {
if (!errors.apiError) return;
return (
<div className="rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{errors.apiError && <div>{errors.apiError?.message}</div>}
</h3>
</div>
</div>
</div>
);
}
function renderFormValidation() {
if (!errors.password && !errors.email) return;
return (
<div className="rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{errors.password && <div>{errors.password?.message}</div>}
</h3>
<h3 className="text-sm font-medium text-red-800">
{errors.email && <div>{errors.email?.message}</div>}
</h3>
</div>
</div>
</div>
);
}
return (
<>
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Create a shiny, new <br></br>Documenso Account{" "}
<svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="rgb(17 24 39 / var(--tw-text-opacity))"
className="w-8 h-8 inline mb-1"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
/>
</svg>
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Create your account and start using<br></br>
state-of-the-art document signing for free.
</p>
</div>
{renderApiError()}
{renderFormValidation()}
<FormProvider {...form}>
<form
onSubmit={form.handleSubmit(signUp)}
onChange={() => {
form.clearErrors();
trigger();
}}
className="mt-8 space-y-6"
>
<input type="hidden" name="remember" defaultValue="true" />
<div className="-space-y-px rounded-md shadow-sm">
<div>
<label htmlFor="email-address" className="sr-only">
Email
</label>
<input
{...register("email")}
id="email-address"
name="email"
type="email"
autoComplete="email"
required
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
placeholder="Email"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
{...register("password", {
minLength: {
value: 7,
message:
"Your password has to be at least 7 characters long.",
},
})}
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<Button
type="submit"
onClick={() => {
form.clearErrors();
}}
className="sgroup relative flex w-full"
>
Create Account
</Button>
<div className="pt-2">
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center"></div>
</div>
</div>
<p className="mt-2 text-center text-sm text-gray-600">
Already have an account?{" "}
<Link
href="/login"
className="font-medium text-neon hover:text-neon"
>
Sign In
</Link>
</p>
</form>
</FormProvider>
</div>
</div>
</>
);
}

6
apps/web/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -1,24 +1,15 @@
/** @type {import('next').NextConfig} */
require("dotenv").config({ path: "../../.env" });
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const nextConfig = {
const { parsed: env } = require('dotenv').config({
path: path.join(__dirname, '../../.env.local'),
});
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
swcMinify: false,
transpilePackages: ['@documenso/lib', '@documenso/prisma', '@documenso/trpc', '@documenso/ui'],
env,
};
const withTM = require("next-transpile-modules")([
"@documenso/prisma",
"@documenso/lib",
"@documenso/ui",
"@documenso/pdf",
"@documenso/features",
"@documenso/signing",
"react-signature-canvas",
]);
const plugins = [];
plugins.push(withTM);
const moduleExports = () =>
plugins.reduce((acc, next) => next(acc), nextConfig);
module.exports = moduleExports;
module.exports = config;

10729
apps/web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,64 +2,43 @@
"name": "@documenso/web",
"version": "0.1.0",
"private": true,
"license": "AGPL-3.0",
"scripts": {
"dev": "next dev",
"dev": "PORT=3000 next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"db-studio": "prisma db studio"
"lint": "next lint"
},
"dependencies": {
"@documenso/pdf": "*",
"@documenso/lib": "*",
"@documenso/prisma": "*",
"@documenso/tailwind-config": "*",
"@documenso/trpc": "*",
"@documenso/ui": "*",
"@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.18.3",
"next-transpile-modules": "^10.0.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",
"@hookform/resolvers": "^3.1.0",
"@tanstack/react-query": "^4.29.5",
"formidable": "^2.1.1",
"framer-motion": "^10.12.8",
"lucide-react": "^0.214.0",
"micro": "^10.0.1",
"next": "13.4.1",
"next-auth": "^4.22.1",
"next-plausible": "^3.7.2",
"next-themes": "^0.2.1",
"perfect-freehand": "^1.2.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-draggable": "^4.4.5",
"react-hook-form": "^7.41.5",
"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"
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.43.9",
"react-icons": "^4.8.0",
"react-pdf": "^7.1.1",
"typescript": "5.0.4",
"zod": "^3.21.4"
},
"devDependencies": {
"@types/formidable": "^2.0.5",
"@types/node": "^18.11.18",
"@types/nodemailer": "^6.4.7",
"@types/nodemailer-sendgrid": "^1.0.0",
"@types/react-pdf": "^6.2.0",
"@types/react-resizable": "^3.0.3",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.19",
"tailwindcss": "^3.2.4"
"@types/formidable": "^2.0.6",
"@types/node": "20.1.0",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4"
}
}

View File

@ -1,36 +0,0 @@
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
import { Button } from "@documenso/ui";
import Logo from "../components/logo";
export default function Custom404() {
return (
<>
<main className="relative min-h-full bg-gray-100 isolate">
<div className="absolute top-10 left-10">
<Logo className="w-10 md:w-20" />
</div>
<div className="px-6 py-48 mx-auto text-center max-w-7xl sm:py-40 lg:px-8">
<p className="text-base font-semibold leading-8 text-brown">404</p>
<h1 className="mt-4 text-3xl font-bold tracking-tight text-brown sm:text-5xl">
Page not found
</h1>
<p className="mt-4 text-base text-gray-700 sm:mt-6">
Sorry, we couldnt find the page youre looking for.
</p>
<div className="flex justify-center mt-10">
<Button
color="secondary"
href="/"
icon={ArrowSmallLeftIcon}
className="text-base font-semibold leading-7 text-brown"
>
Back to home
</Button>
</div>
</div>
</main>
</>
);
}

View File

@ -1,33 +0,0 @@
import Logo from "../components/logo";
import { Button } from "@documenso/ui";
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
export default function Custom500() {
return (
<>
<div className="relative flex flex-col items-center justify-center min-h-full text-white bg-black">
<div className="absolute top-10 left-10">
<Logo dark className="w-10 md:w-20" />
</div>
<div className="px-4 py-10 mt-20 max-w-7xl">
<p className="inline-flex items-center text-3xl font-bold sm:text-5xl">
500
<span className="relative px-3 font-thin sm:text-6xl -top-1.5">
|
</span>{" "}
<span className="text-base font-semibold align-middle sm:text-2xl">
Something went wrong.
</span>
</p>
<div className="flex justify-center mt-10">
<Button color="secondary" href="/" icon={ArrowSmallLeftIcon}>
Back to home
</Button>
</div>
</div>
</div>
</>
);
}

View File

@ -1,30 +0,0 @@
# Docker config for render.com
# Be sure to add an .env config to your docker web service
FROM node:19.5.0-alpine
RUN apk add --no-cache openjdk11
WORKDIR /app
# Inserted from render.com ENV Group
ARG DATABASE_URL
ARG MAIL_FROM
ARG NEXT_PUBLIC_WEBAPP_URL
ARG NEXTAUTH_SECRET
ARG NEXTAUTH_URL
ARG SENDGRID_API_KEY
# Fill docker ENV variables with render.com ENV Group - BUILD-TIME
ENV DATABASE_URL=$DATABASE_URL \
MAIL_FROM=$ \
NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL \
NEXTAUTH_SECRET=&NEXTAUTH_SECRET \
NEXTAUTH_URL=$NEXTAUTH_URL \
SENDGRID_API_KEY=$SENDGRID_API_KEY
COPY . /app
RUN npm run build
# No runtime ENV Variables set so far besides ENV
ENV NODE_ENV production
EXPOSE 3000
CMD ["npm", "start"]

View File

@ -1,31 +0,0 @@
import "../styles/tailwind.css";
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
import "../../../node_modules/react-resizable/css/styles.css";
import "react-tooltip/dist/react-tooltip.css";
import { ReactElement, ReactNode } from "react";
import type { AppProps } from "next/app";
import { NextPage } from "next";
import { SessionProvider } from "next-auth/react";
export { coloredConsole } from "@documenso/lib";
import { Toaster } from "react-hot-toast";
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
export default function App({
Component,
pageProps: { session, ...pageProps },
}: AppPropsWithLayout) {
const getLayout = Component.getLayout || ((page: any) => page);
return (
<SessionProvider session={session}>
<Toaster position="top-center"></Toaster>
{getLayout(<Component {...pageProps} />)}
</SessionProvider>
);
}

View File

@ -1,21 +0,0 @@
import { Head, Html, Main, NextScript } from "next/document";
import Script from "next/script";
export default function Document(props) {
let pageProps = props.__NEXT_DATA__?.props?.pageProps;
return (
<Html
className="h-full bg-gray-100 scroll-smooth font-normal antialiased"
lang="en"
>
<Head>
<meta name="color-scheme"></meta>
</Head>
<body className="flex h-full flex-col">
<Main />
<NextScript />
</body>
</Html>
);
}

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