Compare commits

...

182 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan 3af26291c9 minor changes 2023-07-08 08:56:34 +00:00
Doug Andrade d1bc948f3c clean up console.log() used for testing 2023-06-13 02:00:45 -04:00
Doug Andrade 2b84636993 feat: google auth without schema change 2023-06-13 01:53:12 -04:00
Lucas Smith 05238f096b feat: dark mode & theme switching
feat: dark mode & theme switching
2023-06-12 16:01:02 +10:00
Doug Andrade dd83d4607c fix: dark mode on signup and signin pages 2023-06-11 12:26:47 -04:00
Doug Andrade 07d13c74f5 fix: signature pad in dark mode 2023-06-11 11:36:13 -04:00
Doug Andrade 64d1d6df37 resolving eslint build errors 2023-06-11 02:21:13 -04:00
Doug Andrade 877a579533 adding dark mode to feat/refresh 2023-06-11 01:50:19 -04:00
Mythie b0e364acf4 wip: create document workflow 2023-06-10 22:33:12 +10:00
Mythie 803ebccee3 wip: refresh design 2023-06-09 18:22:21 +10:00
Lucas Smith 5ec97657c1 Merge pull request #199 from documenso/fix/new-slack-link
fix: expired slack link
2023-06-07 20:15:13 +10:00
Ephraim Atta-Duncan 02f9c38e1e Replace slack link with documen.so/slack 2023-06-07 09:59:40 +00:00
Lucas Smith aa651fb4e0 Merge pull request #191 from eltociear/patch-1
Fix typo in pdf-editor.tsx
2023-06-06 19:01:31 +10:00
Ephraim Atta-Duncan 9ff8527336 fix: expired slack link 2023-06-05 21:49:39 +00:00
Lucas Smith 4e65ff3a47 Merge pull request #195 from PeerRich/patch-1
chore: fix readme Product Hunt Badges
2023-06-05 21:47:39 +10:00
Peer Richelsen 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
Ikko Eltociear Ashimine a1bb360b6f Fix typo in pdf-editor.tsx
postion -> position
2023-06-03 21:58:14 +09:00
Lucas Smith 11c1b6841f Merge pull request #185 from ahiho/fix/ipv6
docs: update troubleshooting for IPv6
2023-06-03 00:44:04 +10:00
Thanh Vu c41007e026 Revert "fix: support ipv6 for nextjs"
This reverts commit f9de6671e0aa29e25e872a80aa334d3319e3e522.
2023-06-02 18:04:52 +07:00
Thanh Vu db99bf3674 Revert "fix: custom nextjs server"
This reverts commit 8f9a5f4ec7d834970a3e2b0778ce94218c997a8f.
2023-06-02 18:04:52 +07:00
Thanh Vu 3caa01ab53 Revert "fix: add custom nextjs server to docker"
This reverts commit 5dbe7b26286234db542921d9ded000c522c9a31e.
2023-06-02 18:04:52 +07:00
Thanh Vu 20b618c70f docs: update troubleshooting for IPv6 2023-06-02 18:04:52 +07:00
Thanh Vu bbedd6d3de fix: add custom nextjs server to docker 2023-06-02 18:04:52 +07:00
Thanh Vu 054480500f fix: custom nextjs server 2023-06-02 18:04:52 +07:00
Thanh Vu 15b5f31a74 fix: support ipv6 for nextjs 2023-06-02 18:04:52 +07:00
Mythie 316fb49339 fix: disable subscriptions in example env 2023-06-02 19:03:59 +10:00
Lucas Smith fc1b3be5ad Merge pull request #184 from The-Robin-Hood/bugfix/docker_script_update
docker script updated 🐳
2023-06-02 18:26:21 +10:00
Robinhood 20b51198b4 docker script updated 🐳 2023-06-01 22:24:58 +05:30
Lucas Smith f80edf3f94 Merge pull request #181 from ahiho/fix/docker-image-typo
typo: documentso >> documenso
2023-06-02 00:06:28 +10:00
Lucas Smith 08faabc813 Merge pull request #182 from JonasPardon/patch-1
Fix typos in example env
2023-06-02 00:04:50 +10:00
Mythie 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
Jonas Pardon 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
Thanh Vu dd4568b7fa typo: documentso >> documenso 2023-06-01 13:58:18 +07:00
Lucas Smith 893ab9bea5 Merge pull request #167 from dephraiim/fix/dashboard-logo
fix: dashboard logo
2023-05-30 23:29:57 +10:00
Lucas Smith 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
Ansari 0a5de18235 minor script fix 2023-05-30 15:43:58 +05:30
Mythie b5e03359c1 fix: mark truncateTitle as optional 2023-05-30 20:11:23 +10:00
Lucas Smith a266e4f9d4 Merge pull request #150 from The-Robin-Hood/bugfix/long_filename
Long filename fix 🗄
2023-05-30 20:04:19 +10:00
Lucas Smith eccd9b5cd3 Merge branch 'main' into bugfix/long_filename 2023-05-30 20:03:19 +10:00
Lucas Smith 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
Lucas Smith 6048555e4a Merge branch 'main' into next 2023-05-30 19:55:31 +10:00
Lucas Smith e33f31c483 Merge pull request #165 from doug-andrade/fix-typos
typo: /ressources to /resources
2023-05-30 19:54:32 +10:00
Lucas Smith fe82e3c84f Merge pull request #171 from leerob/turbo
Add turborepo to monorepo.
2023-05-30 19:34:53 +10:00
Ansari 7684a49b7d Merge branch 'main' into bugfix/long_filename 2023-05-30 14:30:55 +05:30
Mythie d8ad4b4b2b fix: add turbo dep and start command 2023-05-30 18:56:41 +10:00
Lucas Smith 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
Lucas Smith 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
Doug Andrade 307b0cc9d9 fixed height on login/signup pages 2023-05-30 02:36:43 -04:00
Doug Andrade 3e94491474 fixed next/font load on ALL pages and toast. 2023-05-30 02:17:34 -04:00
Doug Andrade 007fe44db8 loading fonts with next/font 2023-05-29 22:25:36 -04:00
Lee Robinson 1e6f65f92d Explicit deps 2023-05-29 19:46:24 -05:00
Piyush Maurya 82fbedf8e3 fix: deprecated type checks and imports from @prisma 2023-05-30 00:24:18 +05:30
Lee Robinson 2f3be1cfe5 Add turborepo to monorepo. 2023-05-29 10:38:24 -05:00
Mythie 8ecd5cf215 fix: respect truncate title prop 2023-05-29 18:47:54 +10:00
Lucas Smith 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
Ephraim Atta-Duncan 4c06b5e640 fix: logo only on dashboard 2023-05-29 08:01:12 +00:00
Doug Andrade b477799d70 update: add new brand color as palette in tailwind config 2023-05-28 23:17:52 -04:00
Doug Andrade b928993510 typo: /ressources >> /resources 2023-05-28 20:06:43 -04:00
Doug Andrade ad4d844b4d remove: next-transpile-modules dependency 2023-05-28 19:53:44 -04:00
Doug Andrade 3444d7fd93 task: simplify next.config.js 2023-05-28 19:44:00 -04:00
Timur Ercan 3e220135be fix: readme format 2023-05-28 17:42:54 +02:00
Timur Ercan 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
Timur Ercan 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
Timur Ercan f6bff1649b fix: disable empty state add doc without subscr. 2023-05-28 06:38:55 +02:00
Timur Ercan b2b499f397 chore: update signup link 2023-05-28 06:16:48 +02:00
Mythie eb18a7e11c feat: update password in dashboard 2023-05-28 13:11:09 +10:00
Mythie 89d9e02464 fix: update logo 2023-05-28 13:10:09 +10:00
Mythie a83b09f4db fix: update favicon 2023-05-28 13:07:47 +10:00
Mythie e445830ffb fix: convert readFile to buffer 2023-05-28 08:04:16 +10:00
Mythie bfff81dd3c fix: apply encoding on buffer level 2023-05-28 07:43:02 +10:00
Timur Ercan 02129aab73 Merge pull request #162 from documenso/feature/update-branding
Feature/update branding
2023-05-27 18:23:22 +02:00
Timur Ercan e7386928fa Merge branch 'main' into feature/update-branding 2023-05-27 18:22:13 +02:00
Timur Ercan 7890b4adf1 feat: update logo 2023-05-27 18:18:34 +02:00
Timur Ercan 6aa40b2547 feat: update logo 2023-05-27 18:18:21 +02:00
Timur Ercan c142c1bd54 feat: favicon logo 2023-05-27 18:11:49 +02:00
Mythie 980bd0d485 fix: convert readFile to buffer 2023-05-28 00:53:43 +10:00
Mythie 17d51354d7 fix: support cert file encodings 2023-05-28 00:39:07 +10:00
Lucas Smith 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
Lucas Smith a300c3fb3a Merge branch 'main' into feat/support-custom-cert-paths 2023-05-27 23:38:33 +10:00
Timur Ercan 5e07b8bd92 Merge pull request #161 from documenso/ElTimuro-patch-1
PRODUCT HUNT LAUNCH TOMORROW
2023-05-27 12:04:55 +02:00
Timur Ercan 7b1d626f9a PRODUCT HUNT LAUNCH TOMORROW
WE LAUNCH ON PRODUCT HUNT TOMORROW.
https://dub.sh/documenso-launch
2023-05-27 12:04:44 +02:00
Mythie de46d0f4ab fix: support passphrase env var 2023-05-27 01:31:48 +10:00
Timur Ercan cc7ab171b1 Merge pull request #160 from documenso/ElTimuro-patch-1
Update README.md
2023-05-26 17:23:16 +02:00
Timur Ercan 466941dbc2 Update README.md 2023-05-26 17:22:56 +02:00
Mythie 0564792604 fix: remove unused import 2023-05-27 01:08:33 +10:00
Mythie 32f904ad68 feat: support leading cert from custom path 2023-05-27 01:07:07 +10:00
Mythie 748f842115 fix: update prop typo, extract truncate method 2023-05-25 18:43:37 +10:00
Timur Ercan ecaec356a1 Product Hunt Hint 2023-05-23 17:22:40 +02:00
Timur Ercan 38f730c730 added EE folder and EE license scaffold 2023-05-23 17:09:42 +02:00
Timur Ercan 2b4a9fbe21 Merge pull request #145 from documenso/feat/add-subscriptions
Add subscriptions
2023-05-22 16:43:10 +02:00
Timur Ercan 106ac40fb1 texts, monthly billing default 2023-05-21 21:01:34 +02:00
Timur Ercan 62ac181193 Merge branch 'main' into feat/add-subscriptions 2023-05-21 20:44:07 +02:00
Timur Ercan 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
Timur Ercan 38a8279757 fix 2023-05-21 20:36:38 +02:00
Timur Ercan ed77000746 fix: pass recipient token to signed page 2023-05-21 20:28:06 +02:00
Timur Ercan 73b72c6cce fix NEXT_PUBLIC_ALLOW_SIGNUP reference 2023-05-21 20:10:06 +02:00
Timur Ercan b2aa4d6587 Merge branch 'main' into feat/add-subscriptions 2023-05-21 19:08:23 +02:00
Timur Ercan bde80bf2c9 clean up debug 2023-05-21 18:52:35 +02:00
Mythie 1e505088ad fix: hide billing if the feature flag hasn't been passed 2023-05-21 23:00:54 +10:00
Timur Ercan ae0799168a clean up submodule 2023-05-19 20:02:39 +02:00
Timur Ercan b5ec3cc817 Merge branch 'main' into feat/add-subscriptions 2023-05-19 19:47:23 +02:00
Timur Ercan 4a2162478e Merge branch 'main' into bugfix/long_filename 2023-05-19 19:33:22 +02:00
Timur Ercan 370f38457b Merge pull request #151 from abielzulio/abielzulio-fix-typo
fix(`typo`): `availible` → `available`
2023-05-19 18:58:55 +02:00
Ansari 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
Ansari 8bf6594cf4 prop name changed 📛 2023-05-15 15:43:29 +05:30
Ansari b6ff01ef86 Merge branch 'main' into bugfix/long_filename 2023-05-15 15:40:04 +05:30
Ansari 9b993c08f1 Truncate prop added 2023-05-15 15:39:37 +05:30
Abiel Zulio M f34813e450 Update login.tsx 2023-05-14 20:13:45 +07:00
Lucas Smith 8f6c6dccf4 Merge pull request #148 from piyushkrmaurya/main
Add tooltip to recipients action buttons
2023-05-12 07:42:24 +10:00
Piyush Maurya 826704c21f Adds animation to tooltip 2023-05-11 21:00:05 +05:30
Piyush Maurya 4f47bbb552 Adds consistent 'className' to action buttons 2023-05-11 14:04:40 +05:30
Piyush Maurya 825231fe2a Adds tooltip to recipient action buttons 2023-05-11 12:15:41 +05:30
Ansari 051e681701 Long filename fix 🗄 2023-05-10 20:13:07 +05:30
Lucas Smith 012d2a9a09 Merge pull request #147 from dephraiim/readme-fixes
Readme Fixes
2023-05-09 17:28:27 +10:00
Ephraim Atta-Duncan 85c593d8e3 fix typo 2023-05-09 07:24:38 +00:00
Ephraim Atta-Duncan 0f28692a39 Update readme 2023-05-07 11:25:12 +00:00
Mythie 22bc854cac feat: add warnings for subscription lapses and cancellations 2023-05-06 16:40:37 +10:00
Mythie d2c5657093 fix: update signup env var 2023-05-06 16:11:56 +10:00
Mythie 6934e573d5 feat: add guards and subscription ui 2023-05-06 16:08:21 +10:00
Mythie 7eaa00b836 feat: add stripe api handlers 2023-05-06 14:34:43 +10:00
Mythie e7e881be01 fix: update env types 2023-05-06 14:34:20 +10:00
Mythie e80997f462 fix: update env vars 2023-05-06 14:33:42 +10:00
Mythie da0166b746 fix: tidy stripe feature and add provider 2023-05-06 14:33:27 +10:00
Mythie 900b816ae0 feat: stripe handlers and fetchers 2023-05-05 20:08:18 +10:00
Mythie ed3e4d22ef feat: scaffhold subscription table and ui 2023-05-05 19:29:42 +10:00
Lucas Smith bf84ec8962 Merge pull request #74 from Mythie/chore/optimise-deps
chore: optimise depedency tree
2023-05-01 20:38:24 +10:00
Lucas Smith 1abfa93551 Merge branch 'main' into chore/optimise-deps 2023-05-01 20:36:37 +10:00
Lucas Smith 039cc75882 Merge pull request #139 from documenso/fix/improve-general-styling
chore: improve general styling
2023-05-01 20:05:17 +10:00
Mythie 8457823d8e fix: improve sign in and sign up pages 2023-05-01 20:01:36 +10:00
Mythie 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
Timur Ercan 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
Timur Ercan 108614bf46 Merge branch 'main' into feat/DOC-210-sign-dialog-broken-on-second-opening 2023-04-28 18:22:57 +02:00
Timur Ercan 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
Timur Ercan 82139f6b2d Merge branch 'main' into fix/DOC-214-date-field-appears-for-all-recipients 2023-04-25 11:51:23 +02:00
Lucas Smith 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
Lucas Smith 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
Lucas Smith 64b755d5ba Merge branch 'main' into fix/keep-url-state-in-sync 2023-04-25 10:48:07 +10:00
Lucas Smith 8788b64585 Merge pull request #143 from mikeriss/fix-typo
Fix: typos on Readme
2023-04-25 10:41:27 +10:00
mikeriss c9547057f6 fixed addional typos
typos fixed
2023-04-24 19:59:56 +02:00
mikeriss 17e688c222 typo
changed machnine to machine
2023-04-24 19:51:05 +02:00
mikeriss f5a42e694d Updated README.md typo
changed a typo from signging to signing
2023-04-24 19:48:34 +02:00
Subham Ray b2d09216c8 rename function 2023-04-24 23:13:38 +05:30
Subham Ray 6d30a486ab added type for statusFilter 2023-04-24 19:37:41 +05:30
Subham Ray dc6217b14e feat(Documents Filter): Keep the URL and UI state in sync when status filter changes 2023-04-24 19:16:56 +05:30
Lucas Smith a6171ec4f3 Merge branch 'main' into fix/DOC-214-date-field-appears-for-all-recipients 2023-04-23 10:36:17 +10:00
Timur Ercan d0f962598c Merge branch 'main' into feat/DOC-210-sign-dialog-broken-on-second-opening 2023-04-21 15:49:40 +02:00
Mythie 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
Mythie 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
Timur Ercan ab96990d43 render PR env debug 2023-04-21 13:29:51 +02:00
Timur Ercan ad5b2bcf82 fix: pr env condition 2023-04-21 12:59:53 +02:00
Timur Ercan 6f18be6b5b add render preview env support 2023-04-21 12:42:31 +02:00
Timur Ercan 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
Timur Ercan 4b9840d7e0 Merge branch 'main' into fix/can-add-signature-space-for-empty-recipients 2023-04-20 17:25:39 +02:00
Timur Ercan 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
Timur Ercan 989d036e54 Merge branch 'main' into fix/signing-email-breaks-on-small-decices 2023-04-20 17:14:00 +02:00
Lucas Smith 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
Mythie 70ea3ceaf3 fix: improve types 2023-04-19 23:56:39 +10:00
Saurav Gurhale 80d26adf9c add toast error for invalid email 2023-04-19 23:56:39 +10:00
Lucas Smith 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
Mythie 95c3be9a77 chore: optimise depedency tree 2023-04-19 23:30:14 +10:00
Lucas Smith 52f554a636 Merge pull request #136 from dephraiim/doc-223
Remove Input Placeholders
2023-04-19 22:55:26 +10:00
zahid 849885b5b3 fix: redirect to /dashboard if auth user tries to access /login or /signup 2023-04-19 13:11:02 +06:00
Ephraim Atta-Duncan bcc2530484 Declutter Textarea by removing placeholders 2023-04-16 23:45:57 +00:00
Mythie 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
Ephraim Atta-Duncan 84e3d29589 Use documenso as container name for local development 2023-04-16 18:29:40 +00:00
Mythie ba3ffe68ea fix: disable selection for draft recipients 2023-04-16 23:02:50 +10:00
Timur Ercan 5c58b32d92 Merge branch 'main' of https://github.com/documenso/documenso 2023-04-15 20:35:36 +02:00
Timur Ercan f10bafd998 cleanup 2023-04-15 20:35:33 +02:00
Timur Ercan 2cf8896e46 Merge branch 'main' of https://github.com/documenso/documenso 2023-04-15 20:33:38 +02:00
Timur Ercan e873af3ec9 cleanup 2023-04-15 20:31:38 +02:00
Timur Ercan 06501bde60 cleanup 2023-04-15 20:31:24 +02:00
Timur Ercan 0dcab27e65 fix: openshift build does not allow private repos 2023-04-15 20:26:35 +02:00
Timur Ercan ff2334ab55 fix: openshift build does not allow private repos
https://github.com/documenso/documenso/issues/79
2023-04-15 20:18:30 +02:00
Timur Ercan 63bd044723 feat: npm run d for ultra quick start 2023-04-15 20:04:28 +02:00
Timur Ercan b111874d7c fix: redirect users sessions not found in databae 2023-04-15 19:54:04 +02:00
Timur Ercan 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
Mythie cb77a40fd9 fix: update postgres port 2023-04-13 23:43:42 +10:00
Mythie 7aa7485388 fix: migrate dx.sh to package scripts 2023-04-13 22:52:54 +10:00
Timur Ercan 984084dd3b Merge branch 'main' into feat/docker-environment 2023-04-13 14:50:36 +02:00
Mythie 6ea38efd9d chore: tidy script 2023-04-09 22:36:28 +10:00
Mythie 49cb50ed6e feat: add down flag for stopping environment 2023-04-09 22:33:14 +10:00
Mythie 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
431 changed files with 21001 additions and 27121 deletions
+13 -14
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
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
+13 -27
View File
@@ -1,33 +1,19 @@
# Database
# Option 1: You can 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
# Option 2: Set up a local Postgres SQL instance (RECOMMENDED)
# ⚠ 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 (NODEMAILER)
# SENDGRID
# Get a Sendgrid Api key here: https://signup.sendgrid.com
SENDGRID_API_KEY=''
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
# SMTP
# Set SMTP credentials to use SMTP instead of the Sendgrid API.
SMTP_MAIL_HOST=''
SMTP_MAIL_PORT=''
SMTP_MAIL_USER=''
SMTP_MAIL_PASSWORD=''
NEXT_PRIVATE_STRIPE_API_KEY=
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
# Sender for signing requests and completion mails.
MAIL_FROM='documenso@localhost.com'
NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED=false
#FEATURE FLAGS
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
ALLOW_SIGNUP=true
# This is only required for the marketing site
NEXT_PRIVATE_REDIS_URL=
NEXT_PRIVATE_REDIS_TOKEN=
+13
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/*/'],
},
},
};
+14 -16
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
View File
@@ -1,3 +0,0 @@
[submodule "apps/website/documenso/website"]
path = apps/website/documenso/website
url = http://github.com/documenso/website.git
+1
View File
@@ -0,0 +1 @@
auto-install-peers = true
+6 -15
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
}
+90 -22
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,7 +22,7 @@
</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/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
@@ -56,13 +56,18 @@
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
@@ -71,14 +76,12 @@ The current project goal is to <b>[release a production ready version](https://g
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
## Tools
# Tech
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/)
@@ -86,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
@@ -96,12 +99,39 @@ 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
@@ -111,36 +141,36 @@ Follow these steps to setup documenso on you local machnine:
- 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)
- 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\_\* varibles</code> in your .env
- 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\ressources\example.pdf</code> manually to test your setup
- 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:
- 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 neccessary on first clone
- This is not necessary on first clone.
# Creating your own signging certificate
# Creating your own signing certificate
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:
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>
@@ -151,9 +181,47 @@ For the digital signature of you documents you need a signign certificate in .p1
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>
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
View File
@@ -0,0 +1 @@
# @documenso/marketing
+6
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.
+15
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;
+37
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"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+13
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

+1
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

+19
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"
}
+1
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

@@ -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;
};
@@ -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>;
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
+1
View File
@@ -0,0 +1 @@
@import '@documenso/ui/styles/theme.css';
+53
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

@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
</>
);
};
@@ -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>
);
};
@@ -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,
);
});
}
}
@@ -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;
};
@@ -0,0 +1 @@
export * from './signature-pad';
@@ -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);
}
}
@@ -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
@@ -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];
}
@@ -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',
});
}
}
@@ -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',
});
}
@@ -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>;
};
+11
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}`,
],
};
+26
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"
]
}
-4
View File
@@ -1,4 +0,0 @@
{
"presets": ["next/babel"],
"plugins": []
}
-3
View File
@@ -1,3 +0,0 @@
{
"extends": ["next/babel", "next/core-web-vitals"]
}
+1
View File
@@ -0,0 +1 @@
# @documenso/web
@@ -1,86 +0,0 @@
import React, { useState } from "react";
import { IconButton } from "@documenso/ui";
import Logo from "../logo";
import { XCircleIcon } from "@heroicons/react/20/solid";
import Draggable from "react-draggable";
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="absolute top-0 left-0 m-auto h-16 w-48 cursor-move select-none flex-row-reverse p-2 text-center text-lg font-bold opacity-80"
style={{
background: stc(props.field.Recipient.email),
}}>
<div className="m-auto flex-row-reverse overflow-hidden text-center text-lg font-bold">
{field.type}
{field.type === "SIGNATURE" ? (
<div className="text-center text-xs">
{`${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>
);
}
@@ -1,68 +0,0 @@
import { useEffect, useState } from "react";
import { classNames } from "@documenso/lib";
import { RadioGroup } from "@headlessui/react";
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);
}}>
<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",
"relative block cursor-pointer select-none rounded-lg border bg-white px-3 py-2 hover:bg-slate-100 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="mr-3 inline-block h-4 w-4 flex-shrink-0 rounded-full 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>
);
}
-120
View File
@@ -1,120 +0,0 @@
import { useState } from "react";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/router";
import { createField } from "@documenso/features/editor";
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import FieldTypeSelector from "./field-type-selector";
import RecipientSelector from "./recipient-selector";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
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 || props?.document.Recipient.every((e: any) => !e.email);
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>
<div hidden={!noRecipients} className="rounded-md 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">
This document does not have any recipients. Add recipients to create fields.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<Link
href={NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients"}
className="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600">
Add Recipients
<span aria-hidden="true"> &rarr;</span>
</Link>
</p>
</div>
</div>
</div>
<PDFViewer
style={{
cursor: !noRecipients
? `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();
}}
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 rounded-md border border-slate-300 bg-white py-4 pr-5">
<RecipientSelector
recipients={props?.document?.Recipient}
onChange={setSelectedRecipient}
/>
<hr className="m-3 border-slate-300"></hr>
<FieldTypeSelector
selectedRecipient={selectedRecipient}
onChange={setSelectedFieldType}
/>
</div>
</div>
</>
);
function addField(e: any, page: number) {
if (!selectedRecipient) return;
if (!selectedFieldType) return;
if (noRecipients) return;
const signatureField = createField(e, page, selectedRecipient, selectedFieldType);
createOrUpdateField(props?.document, signatureField).then((res) => {
setFields((prevState) => [...prevState, res]);
});
}
}
-185
View File
@@ -1,185 +0,0 @@
import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { createField } from "@documenso/features/editor";
import { createOrUpdateField, deleteField, signDocument } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import Logo from "../logo";
import SignatureDialog from "./signature-dialog";
import { CheckBadgeIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
import { FieldType } from "@prisma/client";
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((prevState) => [...prevState, 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="-mt-2.5 h-12 w-12"></Logo>
</div>
<div className="ml-3 flex-1 items-center justify-start text-center md:flex md:justify-between">
<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, recipient.token).then((res) => {
setFields((prevState) => [...prevState, 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));
});
}
}
}
-168
View File
@@ -1,168 +0,0 @@
import { Fragment, useState } from "react";
import EditableField from "./editable-field";
import SignableField from "./signable-field";
import { FieldType } from "@prisma/client";
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
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="mt-6 max-w-xs"></div>
<Document
file={props.pdfUrl}
onLoadSuccess={onDocumentLoadSuccess}
options={options}
renderMode="canvas"
className="absolute left-0 right-0 mx-auto w-auto">
{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((field) => field.page === index)
.map((field) =>
props.readonly ? (
<SignableField
onClick={props.onClick}
key={field.id}
field={field}
className="absolute"
onDelete={onDeleteHandler}></SignableField>
) : (
<EditableField
hidden={
field.Signature ||
field.inserted ||
field.type === FieldType.FREE_SIGNATURE
}
key={field.id}
field={field}
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>
</>
);
}
@@ -1,92 +0,0 @@
import { Fragment, useEffect, useState } from "react";
import { classNames } from "@documenso/lib";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
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="focus:border-neon focus:ring-neon relative w-full cursor-default select-none rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-1 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 ? "bg-neon-dark text-white" : "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"
)}>
{`${recipient?.name} <${recipient?.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>
);
}
@@ -1,90 +0,0 @@
import React, { useState } from "react";
import { classNames } from "@documenso/lib";
import { IconButton } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/20/solid";
import Draggable from "react-draggable";
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={(e: any) => {
if (!field?.signature) props.onClick(props.field);
}}
ref={nodeRef}
className={classNames(
"absolute top-0 left-0 m-auto h-16 w-48 select-none flex-row-reverse text-center text-lg font-bold opacity-80",
field.type === "SIGNATURE" ? "cursor-pointer hover:brightness-50" : "cursor-not-allowed"
)}
style={{
background: stc(props.field.Recipient.email),
}}>
<div hidden={field?.signature} className="my-4 font-medium">
{field.type === "SIGNATURE" ? "SIGN HERE" : ""}
{field.type === "DATE" ? <small>Date (filled on sign)</small> : ""}
</div>
<div
hidden={!field?.signature}
className="font-qwigley m-auto w-auto flex-row-reverse text-center text-5xl font-medium">
{field?.signature?.type === "type" ? (
<div className="my-4">{field?.signature.typedSignature}</div>
) : (
""
)}
{field?.signature?.type === "draw" ? (
<img className="h-16 w-48" 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>
);
}
@@ -1,195 +0,0 @@
import { Fragment, useEffect, useState } from "react";
import { classNames } from "@documenso/lib";
import { localStorage } 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 SignatureCanvas from "react-signature-canvas";
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="relative min-h-[350px] 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="mb-3 border-b border-gray-200">
<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:border-gray-300 hover:text-gray-700",
"group inline-flex cursor-pointer items-center border-b-2 py-4 px-1 text-sm font-medium"
)}
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 mb-3 border-b border-gray-300">
<input
value={typedSignature}
onChange={(e) => {
setTypedSignature(e.target.value);
}}
className={classNames(
typedSignature ? "font-qwigley text-4xl" : "",
"focus:border-neon focus:ring-neon mt-14 block h-10 w-full text-center align-bottom text-2xl leading-none"
)}
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="float-left block"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}></IconButton>
<div className="float-right mt-10">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}>
Cancel
</Button>
<Button
className="ml-3"
onClick={() => {
props.onClose({
type: "draw",
signatureImage: signCanvasRef.toDataURL("image/png"),
});
}}
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);
}
}
-43
View File
@@ -1,43 +0,0 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import Navigation from "./navigation";
import { useSession } from "next-auth/react";
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>
</>
);
}
-165
View File
@@ -1,165 +0,0 @@
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import Logo from "./logo";
import { LockClosedIcon } from "@heroicons/react/20/solid";
import { signIn } from "next-auth/react";
import { FormProvider, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
interface LoginValues {
email: string;
password: string;
totpCode: string;
csrfToken: string;
}
export default function Login(props: any) {
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="focus:border-neon focus:ring-neon 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:outline-none 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="focus:border-neon focus:ring-neon 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:outline-none sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<a href="#" className="text-neon hover:text-neon font-medium">
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="text-neon-dark group-hover:text-neon h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover: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>
{props.allowSignup ? (
<p className="mt-2 text-center text-sm text-gray-600">
Are you new here?{" "}
<Link href="/signup" className="text-neon hover:text-neon font-medium">
Create a new Account
</Link>
</p>
) : (
<p className="mt-2 text-center text-sm text-gray-600">
Like Documenso{" "}
<Link
href="https://documenso.com"
className="text-neon hover:text-neon font-medium">
Hosted Documenso will be availible soon
</Link>
</p>
)}
</form>
</FormProvider>
</div>
</div>
{/* <Toaster position="top-center" /> */}
</>
);
}
-19
View File
@@ -1,19 +0,0 @@
import Link from "next/link";
import { classNames } from "@documenso/lib";
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>
</>
);
}
-266
View File
@@ -1,266 +0,0 @@
import { Fragment, useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { getUser } from "@documenso/lib/api";
import Logo from "./logo";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import {
ArrowRightOnRectangleIcon,
Bars3Icon,
BellIcon,
ChartBarIcon,
DocumentTextIcon,
UserCircleIcon,
WrenchIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import avatarFromInitials from "avatar-from-initials";
import { signOut, useSession } from "next-auth/react";
import { toast } from "react-hot-toast";
const navigation = [
{
name: "Dashboard",
href: "/dashboard",
current: false,
icon: ChartBarIcon,
},
{
name: "Documents",
href: "/documents",
current: false,
icon: DocumentTextIcon,
},
{
name: "Settings",
href: "/settings/profile",
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]) ||
router.route.includes(element.href.split("/")[1]);
});
return (
<>
<Disclosure as="nav" className="border-b border-gray-200 bg-white">
{({ open, close }) => (
<>
<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 border-b-2 px-1 pt-1 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}>
<item.icon
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
aria-hidden="true"></item.icon>
{item.name}
</Link>
))}
</div>
</div>
<div
onClick={() => {
document?.getElementById("mb")?.click();
}}
className="hidden cursor-pointer px-3 hover:bg-gray-200 sm:ml-6 sm:flex sm:items-center">
<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="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
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
? "border-teal-500 bg-teal-50 text-teal-700"
: "border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800",
"block border-l-4 py-2 pl-3 pr-4 text-base font-medium"
)}
aria-current={item.current ? "page" : undefined}
onClick={() => {
close();
}}>
{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.href.includes("/settings/profile")
? () => {
close();
}
: 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> */}
</>
);
}
-196
View File
@@ -1,196 +0,0 @@
import { ChangeEvent, useEffect, useState } from "react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { updateUser } from "@documenso/features";
import { getUser } from "@documenso/lib/api";
import { Button } from "@documenso/ui";
import { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
import { useSession } from "next-auth/react";
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-brown text-3xl font-bold leading-tight tracking-tight">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
? "border-neon-dark bg-teal-50 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 flex items-center border-l-4 px-3 py-2 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",
"-ml-1 mr-3 h-6 w-6 flex-shrink-0"
)}
aria-hidden="true"
/>
<span className="truncate">{item.name}</span>
</Link>
))}
</nav>
</aside>
<form
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9"
action="#"
method="POST"
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[0].name}>
{/* 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="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none 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="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none disabled:bg-neutral-100 sm:text-sm"
/>
</div>
</div>
<Button onClick={() => updateUser(user)}>Save</Button>
</div>
</form>
<div
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[1].name}
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
{/* Passwords 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">Password</h2>
<p className="mt-1 text-sm text-gray-500">
Forgot your passwort? Email <b>hi@documenso.com</b> to reset it.
</p>
</div>
</div>
</div>
</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>
);
}
-200
View File
@@ -1,200 +0,0 @@
import Link from "next/link";
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 { 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="mb-1 inline h-8 w-8">
<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="focus:border-neon focus:ring-neon 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:outline-none 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="focus:border-neon focus:ring-neon 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:outline-none 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="text-neon hover:text-neon font-medium">
Sign In
</Link>
</p>
</form>
</FormProvider>
</div>
</div>
</>
);
}
+6
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.
+11 -19
View File
@@ -1,23 +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
View File
File diff suppressed because it is too large Load Diff
+27 -48
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.20.1",
"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"
}
}
-34
View File
@@ -1,34 +0,0 @@
import { Button } from "@documenso/ui";
import Logo from "../components/logo";
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
export default function Custom404() {
return (
<>
<main className="relative isolate min-h-full bg-gray-100">
<div className="absolute top-10 left-10">
<Logo className="w-10 md:w-20" />
</div>
<div className="mx-auto max-w-7xl px-6 py-48 text-center sm:py-40 lg:px-8">
<p className="text-brown text-base font-semibold leading-8">404</p>
<h1 className="text-brown mt-4 text-3xl font-bold tracking-tight 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="mt-10 flex justify-center">
<Button
color="secondary"
href="/"
icon={ArrowSmallLeftIcon}
className="text-brown text-base font-semibold leading-7">
Back to home
</Button>
</div>
</div>
</main>
</>
);
}
-31
View File
@@ -1,31 +0,0 @@
import { Button } from "@documenso/ui";
import Logo from "../components/logo";
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
export default function Custom500() {
return (
<>
<div className="relative flex min-h-full flex-col items-center justify-center bg-black text-white">
<div className="absolute top-10 left-10">
<Logo dark className="w-10 md:w-20" />
</div>
<div className="mt-20 max-w-7xl px-4 py-10">
<p className="inline-flex items-center text-3xl font-bold sm:text-5xl">
500
<span className="relative -top-1.5 px-3 font-thin sm:text-6xl">|</span>{" "}
<span className="align-middle text-base font-semibold sm:text-2xl">
Something went wrong.
</span>
</p>
<div className="mt-10 flex justify-center">
<Button color="secondary" href="/" icon={ArrowSmallLeftIcon}>
Back to home
</Button>
</div>
</div>
</div>
</>
);
}
-30
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"]
-32
View File
@@ -1,32 +0,0 @@
import { ReactElement, ReactNode } from "react";
import { NextPage } from "next";
import type { AppProps } from "next/app";
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
import "../../../node_modules/react-resizable/css/styles.css";
import "../styles/tailwind.css";
import { SessionProvider } from "next-auth/react";
import { Toaster } from "react-hot-toast";
import "react-tooltip/dist/react-tooltip.css";
export { coloredConsole } from "@documenso/lib";
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>
);
}
-18
View File
@@ -1,18 +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 scroll-smooth bg-gray-100 font-normal antialiased" lang="en">
<Head>
<meta name="color-scheme"></meta>
</Head>
<body className="flex h-full flex-col">
<Main />
<NextScript />
</body>
</Html>
);
}
-91
View File
@@ -1,91 +0,0 @@
import { ErrorCode } from "@documenso/lib/auth";
import { verifyPassword } from "@documenso/lib/auth";
import prisma from "@documenso/prisma";
import NextAuth, { Session } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GitHubProvider from "next-auth/providers/github";
export default NextAuth({
secret: process.env.AUTH_SECRET,
pages: {
signIn: "/login",
signOut: "/login",
error: "/auth/error", // Error code passed in query string as ?error=
verifyRequest: "/auth/verify-request", // (used for check email message)
},
providers: [
CredentialsProvider({
id: "credentials",
name: "Documenso.com Login",
type: "credentials",
credentials: {
email: {
label: "Email Address",
type: "email",
placeholder: "john.doe@example.com",
},
password: {
label: "Password",
type: "password",
placeholder: "Select a password. Here is some inspiration: https://xkcd.com/936/",
},
},
async authorize(credentials: any) {
if (!credentials) {
console.error("Credential missing in authorize()");
throw new Error(ErrorCode.InternalServerError);
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email.toLowerCase(),
},
select: {
id: true,
email: true,
password: true,
name: true,
},
});
if (!user) {
throw new Error(ErrorCode.UserNotFound);
}
if (!user.password) {
throw new Error(ErrorCode.UserMissingPassword);
}
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
if (!isCorrectPassword) {
throw new Error(ErrorCode.IncorrectPassword);
}
return {
id: user.id,
email: user.email,
name: user.name,
};
},
}),
],
callbacks: {
async jwt({ token, user, account }) {
return {
...token,
};
},
async session({ session, token }) {
const documensoSession: Session = {
...session,
user: {
...session.user,
},
};
documensoSession.expires;
return documensoSession;
},
},
});

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