Compare commits

..

274 Commits

Author SHA1 Message Date
dc5a1819f1 chore: upgrade to next 14.0.0 2023-10-27 15:14:04 +11:00
ef8fcd4ea9 fix: missing content updates 2023-10-26 16:58:42 +11:00
bf3747cd7b fix: change sign in links 2023-10-26 16:40:25 +11:00
3ff8a99250 fix: update early adopter error log 2023-10-26 14:26:10 +11:00
fb64496c7d fix: update stripe metadata for early adopters 2023-10-26 13:21:31 +11:00
407fa0047c fix: update limits handler 2023-10-26 12:26:29 +11:00
a2902ee7c0 fix: attach document to completed email 2023-10-25 22:34:51 +11:00
7599b84833 fix: limit recipients 2023-10-25 22:31:31 +11:00
769f543be1 fix: improve claim plan flow 2023-10-25 22:29:51 +11:00
d6447ffa82 fix: return response for failed invoices 2023-10-25 13:29:00 +11:00
f29ac73085 fix: update customer handling for checkouts 2023-10-25 13:13:57 +11:00
fd8f6da2c6 fix: update singleplayer add signature to card 2023-10-22 12:40:09 +11:00
0b50176178 fix: limits no longer cache during session changes 2023-10-22 11:18:00 +11:00
c02640e104 fix: optimise pdf viewer rerendering 2023-10-21 13:08:29 +11:00
091960f269 fix: unbreak pdf viewer 2023-10-20 20:14:10 +11:00
594f3ec16a fix: move getFile to client side 2023-10-20 13:42:10 +11:00
a89da06842 fix: update webhook handler 2023-10-20 12:14:39 +11:00
db1d11309f fix: update webhook handler 2023-10-20 12:05:18 +11:00
616cf1c287 chore: update zod to 3.22.4 (#563)
* chore: updated zod 

Signed-off-by: Adithya Krishna <aadithya794@gmail.com>

---------

Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-10-19 21:24:05 +11:00
ba665edb1e fix: UI fixes and improvements (#559)
* chore: add cursor pointer

* fix: tooltip color

* chore: add tooltip

* fix: admin pages in mobile
2023-10-19 21:00:36 +11:00
1c34eddd10 chore: add prisma studio command (#576)
Co-authored-by: pit <pit@pits-MacBook-Pro.local>
2023-10-19 09:19:46 +03:00
1fbf6ed4ba fix: add mode to checkout session 2023-10-19 12:27:01 +11:00
1d6f7f9e37 fix(bug): name field can be updated with spaces #555 (#558) 2023-10-19 12:12:56 +11:00
e3c3ec7825 fix: downgrade react-pdf 2023-10-18 23:51:16 +11:00
08d176c803 Merge pull request #575 from 18feb06/fix-error-invalid-url-on-main-page-on-self-host-#573
fix: error invalid url on main page on self host #573
2023-10-18 23:20:22 +11:00
b952ed9035 fix: support multi env 2023-10-18 23:19:29 +11:00
43062dda12 chore: upgrade to latest next.js version (#553)
* chore: upgrade next.js
* fix: canvas not found error
* chore: upgrade package for marketing
* feat: add isServer conditional
* fix: inverse isServer condition
* fix: normalize packages
* fix: upgrade ee package
* fix: depdency nightmares
* fix: failing seed script
2023-10-18 22:33:02 +11:00
1b53ff9c2d Merge branch 'documenso:feat/refresh' into fix-error-invalid-url-on-main-page-on-self-host-#573 2023-10-18 16:30:57 +05:00
acd3e6d613 fix: invalid url on main page in self host 2023-10-18 16:28:57 +05:00
e33b02df56 fix: truncate long file name in admin dashboard (#572)
Co-authored-by: pit <pit@192-168-0-136.rdsnet.ro>
2023-10-18 08:58:35 +03:00
2c6849ca76 fix: email requesting signature shows "completed document" in preview… (#514) 2023-10-18 15:32:39 +11:00
9434f9e2e4 fix: support mailto link fix (#571) 2023-10-17 17:27:02 +11:00
f6daef7333 Merge pull request #566 from documenso/feat/plan-limits
feat: plan limits
2023-10-17 14:30:18 +11:00
c3df8d4c2a fix: add redirects for v0.9 requests 2023-10-17 13:50:54 +11:00
4b09693862 feat: add safari clipboard copy support (#486) 2023-10-17 12:40:36 +11:00
8d2e50d1fe fix: user avatar on admin documents table (#570)
Co-authored-by: pit <pit@pits-MacBook-Pro.local>
2023-10-16 17:36:12 +03:00
bfc749f30b fix: fix for Accepting signatures or text fields with white space only #551 (#557) 2023-10-16 20:08:45 +11:00
e0d4255700 fix: enable dragging fields (#565) 2023-10-16 19:50:28 +11:00
6ba4ff1c17 fix: build errors 2023-10-16 17:38:41 +11:00
652af26754 fix: exports on next page 2023-10-15 20:32:36 +11:00
093488a67c feat: plan limits 2023-10-15 20:26:32 +11:00
0d026f3476 fix: filter out inactive products 2023-10-14 13:02:36 +11:00
3e89ec1afc Merge pull request #384 from documenso/feat/stripe-free-tier
feat: add Stripe free tier subscription
2023-10-14 12:22:31 +11:00
df0d18fc81 Merge pull request #502 from documenso/feat/add-e2e-testing-playwright
feat: add playwright
2023-10-14 12:21:18 +11:00
dd25c355ff Merge pull request #396 from documenso/feat/admin-ui-manage-instance 2023-10-13 18:18:24 +03:00
6f851833b2 Merge branch 'feat/refresh' into feat/admin-ui-manage-instance 2023-10-13 16:44:44 +02:00
442b089d7f fix: style updates 2023-10-14 00:20:11 +11:00
1c58b21383 Merge branch 'feat/refresh' into feat/add-e2e-testing-playwright 2023-10-14 00:13:41 +11:00
a6e13faf7b fix: quick tweaks 2023-10-13 13:08:39 +00:00
ede9eb052d fix: named exports 2023-10-13 23:56:11 +11:00
fab006078c Merge pull request #554 from documenso/fix/cascade-delete-share-links
fix: add cascade delete for share links
2023-10-13 23:41:13 +11:00
4d5275f915 fix: create custom pricing table 2023-10-13 23:33:40 +11:00
pit
901e83af58 chore: implemented feedback 2023-10-13 12:16:07 +03:00
pit
e1bee1591f chore: implemented feedback 2023-10-13 11:48:52 +03:00
a354c23231 feat: add document share button to marketing (#422) 2023-10-13 14:14:13 +11:00
f728dd13c5 fix: add cascade delete for share links 2023-10-13 12:45:39 +11:00
pit
7927b87259 chore: polished code 2023-10-12 17:07:54 +03:00
pit
55301a9d53 chore: revert back env file name 2023-10-12 12:49:39 +03:00
pit
c0dd57a4d2 chore: implement feedback 2023-10-12 12:19:23 +03:00
pit
cc80773402 chore: implement feedback 2023-10-12 11:44:16 +03:00
c803d2c4ba feat: single-player-mode-polish (#435) 2023-10-12 18:10:52 +11:00
eb5f5f7a90 fix: background color of signature page (#487) 2023-10-12 14:08:26 +11:00
2ea5ff2c94 fix: bypass signature fix (#536) (#547) 2023-10-12 11:33:01 +11:00
pit
bc9a6fa50a chore: implemented feedback 2023-10-11 16:20:04 +03:00
pit
e02ab7d256 chore: implement pr feedback 2023-10-11 12:32:33 +03:00
01e6367b72 Merge branch 'feat/refresh' into feat/stripe-free-tier 2023-10-11 17:24:01 +11:00
565602f8e1 Merge pull request #530 from anikdhabal/issue#518
fix: Add gitpod configuration
2023-10-11 16:56:59 +11:00
pit
9e0d281883 chore: feedback fix 2023-10-10 16:52:58 +03:00
pit
67629dd735 chore: fix eslint issues 2023-10-10 13:57:07 +03:00
pit
2a89278c7b chore: merge feat/refresh 2023-10-10 13:53:22 +03:00
pit
8f4ba6eb8a chore: self-review 2023-10-10 13:50:50 +03:00
8dfcfb99e0 Merge branch 'feat/refresh' into feat/add-e2e-testing-playwright 2023-10-10 11:49:00 +03:00
pit
1299aa51ee chore: move fetching in data-table-users 2023-10-10 11:44:16 +03:00
e0271cace3 feat: delete draft document (#491) 2023-10-10 13:55:58 +11:00
pit
a11440a7f3 chore: tidy up 2023-10-09 13:30:28 +03:00
cc8c4b8297 Merge branch 'feat/refresh' into issue#518 2023-10-09 15:21:33 +05:30
a287aab4f4 chore: disable dependabot for now 2023-10-09 20:35:19 +11:00
pit
4c518df60d chore: remove generic data table 2023-10-09 12:02:55 +03:00
pit
d4ae733e9e chore: add transition and check for empty users array 2023-10-09 11:59:08 +03:00
b5ed703553 Merge pull request #545 from anikdhabal/fix_dotenv-cli
fix: mismatch the version of dotenv-cli
2023-10-09 19:49:07 +11:00
f49880125a fix: mismatch the version of dotenv-cli 2023-10-09 08:41:13 +00:00
8380c357d9 Merge branch 'feat/refresh' into issue#518 2023-10-09 10:03:58 +05:30
4e010c5624 fix : add gittpod configuration 2023-10-09 09:58:12 +05:30
f53cdbace9 fix: frequency focus ring (#533) 2023-10-09 12:04:01 +11:00
b4d04e2ce9 Merge pull request #516 from adityadeshlahre/feat/refresh
fix(script): [fix : DOC-36] Use dotenv for Prisma package scripts #523
2023-10-09 10:12:08 +11:00
2470aeee1f fix: update script, docs and devcontainer 2023-10-08 21:51:15 +11:00
fd07b47325 Merge pull request #526 from mittalsam98/fix/507-signature-modal-center-align
fix: non responsiveness of Add your sign modal
2023-10-07 22:31:18 +11:00
9257a05831 Merge pull request #527 from documenso/docs/render-deploy
docs: add render one click deploy for refresh
2023-10-07 22:25:42 +11:00
1faa6f2944 Merge pull request #528 from documenso/chore/github-templates 2023-10-07 02:35:35 +00:00
5584bbe9ca Merge branch 'feat/refresh' into chore/github-templates 2023-10-07 02:07:54 +00:00
cc65537ea3 fix: Add gitpod configuration 2023-10-06 23:03:13 +05:30
pit
5f14f87406 feat: filter users by name or email 2023-10-06 15:48:05 +03:00
04a80b7c03 fix: add gitpod configuration 2023-10-06 11:06:34 +05:30
pit
2b44e54d99 feat: subscriptions and documents page 2023-10-05 18:35:12 +03:00
c71a89d1b7 fix: Add gitpod configuration 2023-10-05 12:21:34 +00:00
e2abfd2312 Merge branch 'documenso:feat/refresh' into feat/refresh 2023-10-05 15:46:14 +05:30
49d55227e8 fix: sign up with existing account email bug (#517)
* fix: sign up with existing account email bug
2023-10-05 20:59:43 +11:00
0dadec3b8d fix(script): minor change on scipt 2023-10-05 15:26:53 +05:30
e2d8591d66 Merge branch 'documenso:feat/refresh' into feat/refresh 2023-10-05 15:19:42 +05:30
pit
aecc703317 chore: remove this branch 2023-10-05 12:11:17 +03:00
pit
2422c3e7be chore: update e2e tests 2023-10-05 11:56:32 +03:00
pit
4e1994a0c8 chore: update import 2023-10-05 11:46:45 +03:00
pit
a3dce67117 chore: changes 2023-10-05 11:38:51 +03:00
pit
64dcd451e9 chore: add schema location 2023-10-05 10:56:09 +03:00
pit
a85523ecfc chore: change from npm to npx 2023-10-05 10:53:19 +03:00
pit
85b32bb15b chore: install prisma before prisma client 2023-10-05 10:24:03 +03:00
pit
742ad86b10 chore: add remote caching 2023-10-05 10:13:43 +03:00
pit
39ff11a59d chore: use env vars for tests 2023-10-05 09:12:56 +03:00
pit
4f5976479a chore: merge feat/refresh 2023-10-05 08:47:03 +03:00
eac7aa84b0 fix: add defaultValue to SignaturePad to persist signatures (#522)
* fix: add defaultValue to SignaturePad to persist signatures
2023-10-05 12:54:52 +11:00
bd941202c8 changed text of stepper (#513) 2023-10-05 11:25:09 +11:00
b854f0eedc chore: add pull request templates 2023-10-04 20:05:55 +00:00
1814bd4167 chore: add issue template 2023-10-04 20:04:10 +00:00
b6f9d70fec docs: add render one click deploy for refresh 2023-10-04 19:58:07 +00:00
7c54913bf5 fix: non responsiveness of Add your sign modal 2023-10-05 00:09:30 +05:30
e8d5044ac5 chore: typos
typos
2023-10-04 19:12:30 +02:00
ddf097ede3 Merge branch 'adityadeshlahre/documenso' into feat/refresh 2023-10-04 20:05:33 +05:30
1bad85e1d6 Merge branch 'documenso:feat/refresh' into feat/refresh 2023-10-04 20:04:48 +05:30
68458b50d2 fix(script): added script envprisma in root package.json
dotenv loads all environment variable before running prisma:migrate-dev script
2023-10-04 20:04:07 +05:30
e00f28cf87 chore: add code of conduct (#521)
Update contributing guidelines
2023-10-04 18:38:59 +11:00
4cc34ec50a Merge branch 'documenso:feat/refresh' into feat/refresh 2023-10-04 13:00:52 +05:30
693249916d feat: require old password for password reset (#488)
* feat: require old password for password reset
2023-10-04 15:23:57 +11:00
381a248543 fix: update icons (#468)
* fix: update icons
2023-10-04 13:27:36 +11:00
f637381198 style(ui/ux): added margin to dialogprimitive.content & dialogprimitive.close (m-4) 2023-10-04 01:39:16 +05:30
071335cc66 Merge pull request #504 from documenso/chore/team
chore: add new team members
2023-10-03 14:42:26 +02:00
4d4b011146 chore: add new team members 2023-10-03 14:04:29 +02:00
pit
d10713b477 ci: trigger ci 2023-10-03 10:21:48 +01:00
pit
2efaabd2c3 ci: trigger ci 2023-10-03 10:20:39 +01:00
pit
7bc1e9dcc8 chore: add env step in gh action 2023-10-03 10:19:54 +01:00
pit
8848df701c chore: added delete function 2023-10-03 10:09:40 +01:00
pit
2e800d0eed chore: removed lint step 2023-10-03 10:01:44 +01:00
pit
70ecc9a4a8 feat: add playwright 2023-10-03 09:53:47 +01:00
97dfacd133 fix: Error in Pricing Page Validation for Signup Now Modal (#497)
* fix: signup modal validation on close
* fix: restore auto focus input
2023-10-03 18:26:33 +11:00
87a5bab734 fix: signature text overflow truncated for longer signature texts (#489) 2023-10-03 16:58:11 +11:00
pit
b5fc6e1aaf feat: manage documents admin ui 2023-10-02 16:55:04 +01:00
pit
87f70fa290 feat: profile page done 2023-10-02 11:38:04 +01:00
fa61bb660e Merge pull request #438 from documenso/chore/team
chore: and now his watch has ended
2023-10-01 15:41:52 +11:00
83dd079e03 fix: remove unused imports 2023-10-01 15:24:34 +11:00
f7933d8a4d Merge pull request #477 from Hallidayo/button-texts
feat: auth button loading texts
2023-10-01 13:42:19 +11:00
4bd0cfd283 capitalise 2023-09-30 17:00:43 +01:00
86f39f3824 text changes on isSubmitting 2023-09-30 16:50:52 +01:00
94216f5219 Merge pull request #474 from adithyaakrishna/fix/scripts 2023-09-29 21:56:13 +00:00
a2e6187dae fix: typo in script
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-09-30 01:42:09 +05:30
pit
c2cda0f06e feat: update user functionality 2023-09-29 17:26:37 +01:00
pit
f1bc772985 chore: improve the ui 2023-09-29 17:12:02 +01:00
15e3926ce4 Merge pull request #463 from documenso/docs/minor-readme-updatess
docs: readme updates.
2023-09-30 00:48:38 +10:00
69738d7ed3 Merge branch 'feat/refresh' into docs/minor-readme-updatess 2023-09-30 00:48:30 +10:00
649620c1c9 fix: update compose scripts 2023-09-30 00:47:50 +10:00
99df006019 fix: further readme updates 2023-09-30 00:43:45 +10:00
be40e28f47 Update README.md 2023-09-29 14:42:26 +02:00
8899d82e1b Merge pull request #462 from adithyaakrishna/issue#461
fix: sharp corners of signing card
2023-09-29 18:12:00 +10:00
0527641a4f fix: dogfood resend transport 2023-09-29 12:17:41 +10:00
54e32af1d4 docs: clearer readme? maybe? 2023-09-28 22:48:02 +00:00
b1645ec09a fix: sharp corners of signing card 2023-09-29 02:01:04 +05:30
5b9ce55a6d Merge pull request #457 from documenso/feat-early-adopters
chore: sync
2023-09-28 21:29:09 +02:00
833584bd4f Merge branch 'feat/refresh' into feat-early-adopters 2023-09-28 21:28:41 +02:00
ca52a22bcd chore: sync 2023-09-28 14:59:41 +02:00
ba611b9adb Merge pull request #440 from PeterKwesiAnsah/chore/add-empty-recipientlist-message
chore: add empty message
2023-09-28 22:12:21 +10:00
e8643850a0 Merge pull request #397 from documenso/feat-early-adopters
feat: early adopters
2023-09-28 13:16:21 +02:00
bec7376fe8 Merge branch 'feat/refresh' into feat-early-adopters 2023-09-28 13:16:09 +02:00
7363bbc8f7 chore: sync next 2023-09-28 13:15:56 +02:00
d0f679e177 Merge pull request #430 from documenso/feat/update-email-templates
feat: update email templates
2023-09-28 20:58:22 +10:00
e9769904ab Merge pull request #453 from documenso/fix/443-validation-with-multiple-recipients
fix: add hack for root zod validation with hook form
2023-09-28 20:57:21 +10:00
a2746742a7 Merge remote-tracking branch 'origin/feat/refresh' into feat-early-adopters 2023-09-28 12:35:10 +02:00
459e3dc45b Merge pull request #400 from documenso/feat/open-early-adopters
feat: add early adopters graph
2023-09-28 12:17:03 +02:00
21fedab707 Merge branch 'feat/refresh' into feat/open-early-adopters 2023-09-28 12:16:27 +02:00
323f1974ab chore: added classname and changed typo 2023-09-28 11:13:02 +01:00
5bc9d625c2 Merge pull request #455 from documenso/fix/432-signee-doc-version-doesnt-have-sticky-signing-area
fix: resolve issues with signing document stickiness
2023-09-28 17:50:04 +10:00
4885cf5154 fix: resolve issues with signing document stickiness 2023-09-28 17:42:01 +10:00
2f66eca925 Merge pull request #454 from documenso/fix/446-cancel-cta-does-nothing-when-a-signer-opens-the-document
fix: disable cancel button when there is no window history
2023-09-28 17:15:41 +10:00
af042a62cd fix: disable cancel button when there is no window history 2023-09-28 15:45:22 +10:00
2bfc2b0c1f fix: add hack for root zod validation with hook form 2023-09-28 15:32:03 +10:00
df8bdda718 Merge pull request #450 from documenso/feat/resend-transport
feat: add resend mail transport
2023-09-28 14:07:18 +10:00
873f99ae86 fix: resolve document title inconsistency (#452) 2023-09-28 13:56:22 +10:00
17fe135027 Merge pull request #451 from documenso/fix/445-signer-name-not-persisting
fix: do not overwrite new names or emails for signers
2023-09-28 13:07:26 +10:00
2eed0ae063 feat: add posthog reverse proxy (#449) 2023-09-28 12:56:53 +10:00
f4ae0389d8 fix: do not overwrite new names or emails for signers 2023-09-28 12:35:21 +10:00
9bdff9a61f feat: add resend mail transport 2023-09-28 12:27:04 +10:00
f003a2864e chore: add empty message 2023-09-27 13:08:59 +01:00
ab26af19b8 Merge branch 'feat/refresh' into chore/team 2023-09-27 13:09:26 +02:00
dc512600dc chore: and now his watch has ended 2023-09-27 13:08:19 +02:00
cdb71c3a62 chore: greetings 2023-09-27 12:30:11 +02:00
39d7b3ca58 Merge branch 'feat/refresh' of https://github.com/documenso/documenso into feat/refresh 2023-09-27 12:19:14 +02:00
748c3636d5 chore: sync shop article and add missing and updated assets 2023-09-27 12:15:44 +02:00
6e2d1fb148 fix: add missing URL to email template 2023-09-27 17:44:29 +10:00
7bd847c0d3 chore: add missing migrations 2023-09-27 16:53:48 +10:00
81f9b2f776 Merge branch 'feat/refresh' into feat/update-email-templates 2023-09-27 15:50:54 +10:00
334671ef85 feat: add email forgot password action
Updated email template imports
2023-09-27 15:34:16 +10:00
a20b1e2f6a Merge pull request #350 from documenso/feat/redirect-signed-document
feat: redirect signed document to completed page
2023-09-27 15:09:07 +10:00
572f9d5ad6 chore: styling updates 2023-09-27 14:58:10 +10:00
e49c102e8c fix: lint errors 2023-09-27 09:11:56 +10:00
2348221b03 fix: faster tooltips 2023-09-27 09:09:53 +10:00
a0c327cfcf fix: share og updates 2023-09-27 09:03:53 +10:00
ddbeb9e3db fix: twitter images 2023-09-27 08:02:51 +10:00
e8205c1390 fix: better share links 2023-09-27 07:52:24 +10:00
fbb53fc8c3 chore: update mania shirt res 2023-09-26 22:06:41 +02:00
d7fed5a5dc feat: shop Article 2023-09-26 21:49:04 +02:00
3285881586 chore: typos 2023-09-26 11:39:30 +02:00
3ff61607a2 Merge pull request #370 from documenso/feat/mania
feat: malfunction mania blog post
2023-09-26 11:24:24 +02:00
2a124b03e9 Merge branch 'feat/refresh' into feat/mania 2023-09-26 11:24:13 +02:00
e1aa23bc55 chore: fix typo 2023-09-26 11:23:39 +02:00
818c5c7ba4 chore: fix lightmode logo 2023-09-26 11:19:32 +02:00
668011d1c7 Update README.md with M̸͍͚̽͒A̴̯͊͌L̴͖͖͘F̵̗̻́̅U̶̲̅͠N̸͙̰̓C̵̙̮̾T̸̜̙͌Í̷͎̯Ö̵̘̜́N̴̳͊̈ͅ ̶͔́M̸̡͐̾A̵̞̚N̴̤̏́Ǐ̸̩͂Ă̸͓͝ 2023-09-26 11:12:59 +02:00
562fd043a9 chore: typo 2023-09-26 10:31:41 +02:00
b4781c011c chore: alt text 2023-09-26 10:27:19 +02:00
5c7d3d5503 chore: update date 2023-09-26 10:26:16 +02:00
0f2b6c0ebf chore: remove unused image 2023-09-26 10:23:28 +02:00
ab5bdfeae4 chore: typo 2023-09-26 10:18:56 +02:00
e5fa2f4490 feat: update email templates
Add SPM email attachment
2023-09-26 17:48:26 +10:00
2d2615447e fix: redirectless authentication 2023-09-26 16:17:01 +10:00
46465acd73 chore: quieten dependabot 2023-09-26 16:11:06 +10:00
2dea9ec3e7 Merge pull request #423 from documenso/feat/copy-or-tweet
feat: add dropdown to tweet or copy signing link
2023-09-26 16:08:43 +10:00
584d5bd8ea fix: update share preview 2023-09-26 15:58:43 +10:00
e4b6d42672 fix: styling updates 2023-09-26 15:56:11 +10:00
109ad190cf fix: text 2023-09-25 19:13:03 +02:00
78498793fa chore: refactor function 2023-09-25 11:21:39 +00:00
b2e916378d feat: add dropdown to tweet or copy signing link 2023-09-25 11:15:31 +00:00
d950634aab Merge branch 'feat/refresh' into feat/open-early-adopters 2023-09-25 09:28:04 +02:00
a52c19355a chore: sign document 2023-09-25 15:57:10 +10:00
e67e96333b fix: dark mode for generic mdx content 2023-09-25 11:31:26 +10:00
7aa75b0c64 Merge pull request #403 from documenso/feat/single-player-mode
feat: single player mode
2023-09-25 11:25:58 +10:00
9b1069f208 fix: remove usage of buffer 2023-09-25 11:24:55 +10:00
abe20b8dcf fix: assorted updates 2023-09-25 11:17:59 +10:00
58509c54a9 fix: feature flag client endpoint 2023-09-25 10:35:18 +10:00
fd545cee0c chore: update env.example 2023-09-25 10:33:38 +10:00
1f92bffe7d chore: remove console.log 2023-09-25 10:09:02 +10:00
5615627001 Merge pull request #419 from documenso/fix/incorporation
fix: company name
2023-09-24 21:53:10 +02:00
78c55875ef fix: company name 2023-09-24 21:52:09 +02:00
a3674947b8 fix: single player dark mode and animation updates 2023-09-25 00:18:41 +10:00
63cc57b035 fix: improve dark mode background patterns 2023-09-25 00:18:15 +10:00
d4bc1eb0d1 fix: cors for feature flags 2023-09-25 00:16:01 +10:00
c9f5496acb Merge remote-tracking branch 'origin/feat/refresh' into feat/single-player-mode 2023-09-24 22:18:01 +10:00
99481b6144 feat: darker dark theme 2023-09-24 14:45:50 +10:00
cee147bc9a fix: normalize recipients 2023-09-24 11:46:36 +10:00
3a825391b9 Merge pull request #399 from captain-Akshay/feat/handle_click
fix: cancel button handler
2023-09-23 22:19:57 +10:00
bb347e4614 Merge pull request #326 from documenso/feat/completed-share-link
feat: signing completed sharing link
2023-09-23 22:18:17 +10:00
d0fad4775a feat: add extra info for the early adopters 2023-09-23 12:02:37 +00:00
8012d665ae fix: firefox signing fields 2023-09-23 13:25:39 +10:00
0732aa317d chore: rename community plan to early adopters 2023-09-22 18:54:25 +02:00
fde53e355f chore: staging 2023-09-22 18:44:34 +02:00
8e0a10298e Merge branch 'feat/refresh' into feat/mania 2023-09-22 18:33:16 +02:00
eef63c9adc refactor: metrics into reusable component 2023-09-22 14:08:25 +00:00
c3d52c68e7 feat: add early adopters graph 2023-09-22 13:27:10 +00:00
764c6b88c5 feat: update success page text 2023-09-22 16:35:49 +10:00
989d7a351f feat: add uninserted field validation 2023-09-22 16:25:09 +10:00
679fb80f9a fix: cancel button handler 2023-09-22 10:31:42 +05:30
dc49277bf9 feat: add uninserted field validation 2023-09-22 12:27:39 +10:00
pit
07bf780c3e feat: build individual user page 2023-09-21 15:10:20 +01:00
190ae18edc chore: grammerly 2023-09-21 15:09:20 +02:00
82ffbeeb21 chore: links 2023-09-21 15:01:02 +02:00
9affd7b7fa Merge branch 'feat/refresh' into feat-early-adopters 2023-09-21 14:54:45 +02:00
897ba629df feat: early adopter article 2023-09-21 14:53:48 +02:00
pit
775de16d0a feat: admin ui for managing instance 2023-09-21 12:43:36 +01:00
7eed5c7c96 feat: utilise transport layer 2023-09-20 15:55:25 +10:00
1c629af651 fix: timeout issues 2023-09-20 15:33:01 +10:00
4b8aa3298b feat: add single player mode 2023-09-20 13:48:30 +10:00
4d485940ea fix: stripe customer fetch logic 2023-09-19 15:30:58 +10:00
cbe118b74f fix: merge issues 2023-09-19 15:14:47 +10:00
de9116e9b2 Merge branch 'feat/refresh' into feat/stripe-free-tier 2023-09-19 15:12:40 +10:00
eaa3e0a303 Merge branch 'feat/refresh' into feat/mania 2023-09-18 20:15:50 +02:00
a283c88d7f chore: update malfunction mania image 2023-09-18 20:11:27 +02:00
027a588604 feat: wip 2023-09-18 22:47:46 +10:00
773566f193 feat: add free tier Stripe subscription 2023-09-18 22:33:07 +10:00
c2a9647c90 Merge branch 'feat/refresh' into feat/redirect-signed-document 2023-09-17 14:52:44 +00:00
02424596db fix: update for code review 2023-09-17 14:45:22 +00:00
5809109c05 chore: grammerly, finetuned bounties 2023-09-14 15:43:17 +02:00
a6400eb6c9 Merge branch 'feat/refresh' into feat/mania 2023-09-14 15:29:55 +02:00
39958ed22c Fix typo 2023-09-14 14:57:11 +02:00
7c4ba1b1ea Fix typo 2023-09-14 11:38:42 +02:00
f588897531 Fix punctuation for consistency 2023-09-14 11:33:11 +02:00
5629e08f83 Add keywords
added keywords in description for SEO optimizations
2023-09-14 11:06:48 +02:00
37394c054c Fix typo
Fix typo and minor edits for consistency
2023-09-14 11:04:10 +02:00
b7c0df67b1 feat: malfunction mania, first draft 2023-09-11 18:20:17 +02:00
e47ab838c5 chore: remove undefined check 2023-09-06 07:37:03 +00:00
319 changed files with 12480 additions and 5941 deletions

View File

@ -9,10 +9,5 @@ npm install
# Copy the env file # Copy the env file
cp .env.example .env cp .env.example .env
# Source the env file, export the variables
set -a
source .env
set +a
# Run the migrations # Run the migrations
npm run -w @documenso/prisma prisma:migrate-dev npm run prisma:migrate-dev

View File

@ -6,7 +6,7 @@ NEXTAUTH_SECRET="secret"
NEXT_PRIVATE_GOOGLE_CLIENT_ID="" NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET="" NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
# [[APP]] # [[URLS]]
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000" NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001" NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
@ -15,6 +15,11 @@ NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documen
# Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool. # Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool.
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso" NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
# [[E2E Tests]]
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_password"
# [[STORAGE]] # [[STORAGE]]
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3 # OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
NEXT_PUBLIC_UPLOAD_TRANSPORT="database" NEXT_PUBLIC_UPLOAD_TRANSPORT="database"
@ -50,7 +55,9 @@ NEXT_PRIVATE_SMTP_SECURE=
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso" NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
# REQUIRED: Defines the email address to use as the from address. # REQUIRED: Defines the email address to use as the from address.
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
# OPTIONAL: The API key to use for the MailChannels proxy endpoint. # OPTIONAL: The API key to use for Resend.com
NEXT_PRIVATE_RESEND_API_KEY=
# OPTIONAL: The API key to use for MailChannels.
NEXT_PRIVATE_MAILCHANNELS_API_KEY= NEXT_PRIVATE_MAILCHANNELS_API_KEY=
# OPTIONAL: The endpoint to use for the MailChannels API if using a proxy. # OPTIONAL: The endpoint to use for the MailChannels API if using a proxy.
NEXT_PRIVATE_MAILCHANNELS_ENDPOINT= NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=
@ -66,6 +73,7 @@ NEXT_PRIVATE_STRIPE_API_KEY=
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET= NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID= NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID= NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID=
# [[FEATURES]] # [[FEATURES]]
# OPTIONAL: Leave blank to disable PostHog and feature flags. # OPTIONAL: Leave blank to disable PostHog and feature flags.

50
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,50 @@
---
name: Bug Report
about: Create a bug report to help us improve
---
<!--- Please provide a general summary of the issue in the Title above -->
## Issue Description
<!--- Please provide a clear and concise description of the problem. -->
## Steps to Reproduce
<!--- Please provide step-by-step instructions to reproduce the issue. -->
<!--- Include code snippets, error messages, and any other relevant information. -->
1. Step 1
2. Step 2
3. ...
## Expected Behavior
<!--- Describe what you expected to happen. -->
## Current Behavior
<!--- Describe what is currently happening. -->
## Screenshots (optional)
<!--- If applicable, add screenshots to help explain the issue. -->
## Environment
<!--- Please provide information about your environment, such as operating system, browser, version, etc. -->
- OS: [e.g., Windows 10]
- Browser: [e.g., Chrome, Firefox]
- Version: [e.g., 2.0.1]
## Checklist
<!--- Please check the boxes that apply to this issue report. -->
<!--- You can add or remove items as needed. -->
- [ ] I have searched the existing issues to make sure this is not a duplicate.
- [ ] I have provided steps to reproduce the issue.
- [ ] I have included relevant environment information.
- [ ] I have included any relevant screenshots.
- [ ] I understand that this is a voluntary contribution and that there is no guarantee of resolution.

View File

@ -0,0 +1,41 @@
---
name: Feature Request
about: Suggest a new idea or enhancement for this project
---
<!--- Please provide a clear and concise title for your feature request -->
## Feature Description
<!--- Describe the feature you are requesting in detail. -->
<!--- Explain what problem it solves or what value it adds to the project. -->
## Use Case
<!--- Provide a scenario or use case where this feature would be beneficial. -->
<!--- Explain how users would interact with this feature and why it's important. -->
## Proposed Solution
<!--- If you have an idea of how this feature could be implemented, describe it here. -->
<!--- Include any technical details, UI/UX considerations, or design suggestions. -->
## Alternatives (optional)
<!--- Are there any alternative ways to achieve the same goal? -->
<!--- Describe other approaches that could be considered if this feature is not implemented. -->
## Additional Context
<!--- Add any additional context or information that might be relevant to the feature request. -->
## Checklist
<!--- Please check the boxes that apply to this feature request. -->
<!--- You can add or remove items as needed. -->
- [ ] I have searched the existing feature requests to make sure this is not a duplicate.
- [ ] I have provided a detailed description of the requested feature.
- [ ] I have explained the use case or scenario for this feature.
- [ ] I have included any relevant technical details or design suggestions.
- [ ] I understand that this is a suggestion and that there is no guarantee of implementation.

41
.github/ISSUE_TEMPLATE/improvement.md vendored Normal file
View File

@ -0,0 +1,41 @@
---
name: General Improvement
about: Suggest a minor enhancement or improvement for this project
---
<!--- Please provide a clear and concise title for your improvement suggestion -->
## Improvement Description
<!--- Describe the improvement you are suggesting in detail. -->
<!--- Explain what specific aspect of the project it addresses or enhances. -->
## Rationale
<!--- Explain why this improvement would be beneficial. -->
<!--- Share any context, pain points, or reasons for suggesting this change. -->
## Proposed Solution
<!--- If you have a suggestion for how this improvement could be implemented, describe it here. -->
<!--- Include any technical details, design suggestions, or other relevant information. -->
## Alternatives (optional)
<!--- Are there any alternative approaches to achieve the same improvement? -->
<!--- Describe other ways to address the issue or enhance the project. -->
## Additional Context
<!--- Add any additional context or information that might be relevant to the improvement suggestion. -->
## Checklist
<!--- Please check the boxes that apply to this improvement suggestion. -->
<!--- You can add or remove items as needed. -->
- [ ] I have searched the existing issues and improvement suggestions to avoid duplication.
- [ ] I have provided a clear description of the improvement being suggested.
- [ ] I have explained the rationale behind this improvement.
- [ ] I have included any relevant technical details or design suggestions.
- [ ] I understand that this is a suggestion and that there is no guarantee of implementation.

View File

@ -0,0 +1,49 @@
---
name: Pull Request
about: Submit changes to the project for review and inclusion
---
## Description
<!--- Describe the changes introduced by this pull request. -->
<!--- Explain what problem it solves or what feature/fix it adds. -->
## Related Issue
<!--- If this pull request is related to a specific issue, reference it here using #issue_number. -->
<!--- For example, "Fixes #123" or "Addresses #456". -->
## Changes Made
<!--- Provide a summary of the changes made in this pull request. -->
<!--- Include any relevant technical details or architecture changes. -->
- Change 1
- Change 2
- ...
## Testing Performed
<!--- Describe the testing that you have performed to validate these changes. -->
<!--- Include information about test cases, testing environments, and results. -->
- Tested feature X in scenario Y.
- Ran unit tests for component Z.
- Tested on browsers A, B, and C.
- ...
## Checklist
<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->
- [ ] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these changes.
- [ ] I have updated the documentation to reflect these changes, if applicable.
- [ ] I have followed the project's coding style guidelines.
- [ ] I have addressed the code review feedback from the previous submission, if applicable.
## Additional Notes
<!--- Provide any additional context or notes for the reviewers. -->
<!--- This might include details about design decisions, potential concerns, or anything else relevant. -->

View File

@ -0,0 +1,40 @@
---
name: Test Addition
about: Submit a new test, either unit or end-to-end (E2E), for review and inclusion
---
## Description
<!--- Provide a clear and concise description of the new test you are adding. -->
<!--- Explain the purpose of the test and what it aims to validate. -->
## Related Issue
<!--- If this test addition is related to a specific issue, reference it here using #issue_number. -->
<!--- For example, "Fixes #123" or "Addresses #456". -->
## Test Details
<!--- Describe the details of the test you're adding. -->
<!--- Include information about inputs, expected outputs, and any specific scenarios. -->
- Test Name: Name of the test
- Type: [Unit / E2E]
- Description: Brief description of what the test checks
- Inputs: What inputs the test uses (if applicable)
- Expected Output: What output or behavior the test expects
## Checklist
<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->
- [ ] I have written the new test and ensured it works as intended.
- [ ] I have added necessary documentation to explain the purpose of the test.
- [ ] I have followed the project's testing guidelines and coding style.
- [ ] I have addressed any review feedback from previous submissions, if applicable.
## Additional Notes
<!--- Provide any additional context or notes for the reviewers. -->
<!--- This might include explanations about the testing approach or any potential concerns. -->

View File

@ -9,7 +9,7 @@ updates:
labels: labels:
- "ci dependencies" - "ci dependencies"
- "ci" - "ci"
open-pull-requests-limit: 10 open-pull-requests-limit: 0
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/apps/marketing" directory: "/apps/marketing"
@ -19,7 +19,7 @@ updates:
labels: labels:
- "npm dependencies" - "npm dependencies"
- "frontend" - "frontend"
open-pull-requests-limit: 10 open-pull-requests-limit: 0
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/apps/web" directory: "/apps/web"
@ -29,4 +29,4 @@ updates:
labels: labels:
- "npm dependencies" - "npm dependencies"
- "frontend" - "frontend"
open-pull-requests-limit: 10 open-pull-requests-limit: 0

51
.github/workflows/e2e-tests.yml vendored Normal file
View File

@ -0,0 +1,51 @@
name: Playwright Tests
on:
push:
branches: [feat/refresh]
pull_request:
branches: [feat/refresh]
jobs:
e2e_tests:
timeout-minutes: 60
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Copy env
run: cp .env.example .env
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Generate Prisma Client
run: npm run prisma:generate -w @documenso/prisma
- name: Create the database
run: npm run prisma:migrate-dev
- name: Run Playwright tests
run: npm run ci
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
env:
NEXT_PRIVATE_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
NEXT_PRIVATE_DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}

1
.gitignore vendored
View File

@ -31,6 +31,7 @@ yarn-error.log*
# turbo # turbo
.turbo .turbo
.turbo-cookie
# vercel # vercel
.vercel .vercel

55
.gitpod.yml Normal file
View File

@ -0,0 +1,55 @@
tasks:
- init: |
npm i &&
npm run dx:up &&
cp .env.example .env &&
set -a; source .env &&
export NEXTAUTH_URL="$(gp url 3000)" &&
export NEXT_PUBLIC_WEBAPP_URL="$(gp url 3000)" &&
export NEXT_PUBLIC_MARKETING_URL="$(gp url 3001)"
command: npm run d
ports:
- port: 3000
visibility: public
onOpen: open-preview
- port: 3001
visibility: public
onOpen: open-preview
- port: 9000
visibility: public
onOpen: ignore
- port: 1100
visibility: private
onOpen: ignore
- port: 2500
visibility: private
onOpen: ignore
- port: 54320
visibility: private
onOpen: ignore
github:
prebuilds:
master: true
pullRequests: true
pullRequestsFromForks: true
addCheck: true
addComment: true
addBadge: true
vscode:
extensions:
- aaron-bond.better-comments
- bradlc.vscode-tailwindcss
- dbaeumer.vscode-eslint
- esbenp.prettier-vscode
- mikestead.dotenv
- unifiedjs.vscode-mdx
- GitHub.copilot-chat
- GitHub.copilot-labs
- GitHub.copilot
- GitHub.vscode-pull-request-github
- Prisma.prisma
- VisualStudioExptTeam.vscodeintellicode

126
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,126 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
support@documenso.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].

View File

@ -5,20 +5,36 @@ If you plan to contribute to Documenso, please take a moment to feel awesome ✨
## Before getting started ## Before getting started
- Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission. - Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission.
- Select and issue from [here](https://github.com/documenso/documenso/issues) or create a new one - Select an issue from [here](https://github.com/documenso/documenso/issues) or create a new one
- Consider the results from the discussion in the issue - Consider the results from the discussion on the issue
- Accept the [Contributor License Agreement](https://documen.so/cla) to ensure we can accept your contributions. - Accept the [Contributor License Agreement](https://documen.so/cla) to ensure we can accept your contributions.
## Taking issues
Before taking an issue, ensure that:
- The issue has been assigned the public label
- The issue is clearly defined and understood
- No one has been assigned to the issue
- No one has expressed intention to work on it
You can then:
1. Comment on the issue with your intention to work on it
2. Begin work on the issue
Always feel free to ask questions or seek clarification on the issue.
## Developing ## Developing
The development branch is <code>main</code>. All pull request should be made against this branch. If you need help getting started, [join us on Discord](https://documen.so/discord). The development branch is <code>main</code>. All pull requests should be made against this branch. If you need help getting started, [join us on Discord](https://documen.so/discord).
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
own GitHub account and then own GitHub account and then
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch: 2. Create a new branch:
- Create a new branch (include the issue id and somthing readable): - Create a new branch (include the issue id and something readable):
```sh ```sh
git checkout -b doc-999-my-feature-or-fix git checkout -b doc-999-my-feature-or-fix
@ -29,7 +45,7 @@ The development branch is <code>main</code>. All pull request should be made aga
## Building ## Building
> **Note** > **Note**
> Please be sure that you can make a full production build before pushing code or creating PRs. > Please ensure you can make a full production build before pushing code or creating PRs.
You can build the project with: You can build the project with:

286
README.md
View File

@ -1,8 +1,6 @@
<p align="center" style="margin-top: 120px"> <img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
<a href="https://github.com/documenso/documenso">
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/cd7823ec-4baa-40b9-be78-4acb3b1c73cb" alt="Documenso Logo">
</a>
<p align="center" style="margin-top: 20px">
<p align="center"> <p align="center">
The Open Source DocuSign Alternative. The Open Source DocuSign Alternative.
<br> <br>
@ -16,62 +14,69 @@
<a href="https://github.com/documenso/documenso/issues">Issues</a> <a href="https://github.com/documenso/documenso/issues">Issues</a>
· ·
<a href="https://github.com/documenso/documenso/milestones">Roadmap</a> <a href="https://github.com/documenso/documenso/milestones">Roadmap</a>
·
<a href="https://documen.so/launches">Upcoming Launches</a>
</p> </p>
</p> </p>
<p align="center"> <p align="center">
<a href="https://documen.so/discord"><img src="https://img.shields.io/badge/Discord-documen.so/discord-%235865F2" alt="Join Documenso on Discord"></a> <a href="https://documen.so/discord"><img src="https://img.shields.io/badge/Discord-documen.so/discord-%235865F2" alt="Join Documenso on Discord"></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/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/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> <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>
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/documenso/documenso">
<img alt="open in devcontainer" src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Enabled&color=blue&logo=visualstudiocode" />
</a>
<a href="code_of_conduct.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant"></a>
</p> </p>
> **🚧 We're currently working on a large scale refactor which can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.** > 🦺 Documenso 1.0 is deployed to our <a href="https://documen.so/staging" target="_blank">Staging Environment</a>.
> >
> **[Read more on why 👀](https://documenso.com/blog/why-were-doing-a-rewrite)** > The code can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.
>
> The new version will be released after the current testing phase.
# Documenso 0.9 - Developer Preview # Join us in testing Documenso 1.0 during [MALFUNCTION MANIA](https://documenso.com/blog/malfunction-mania)
<div> <div>
<img style="display: block; height: 120px; width: 24%" <img style="display: block; height: 120px; width: 24%"
src="https://user-images.githubusercontent.com/1309312/224570645-167128ee-3e39-4578-85d2-5394d9a0379c.png"> src="https://github.com/documenso/documenso/assets/1309312/67e08c98-c153-4115-aa2d-77979bb12c94)">
<img style="display: block; height: 120px; width: 24%" <img style="display: block; height: 120px; width: 24%"
src="https://user-images.githubusercontent.com/1309312/224570651-0afd12f8-cfe3-49d1-805e-e495af963d91.png"> src="https://github.com/documenso/documenso/assets/1309312/040cfbae-3438-4ca3-87f2-ce52c793dcaf">
<img style="display: block; height: 120px; width: 24%" <img style="display: block; height: 120px; width: 24%"
src="https://user-images.githubusercontent.com/1309312/224570655-328d2279-058d-4a3e-b5c3-5cbd8a1f4e05.png"> src="https://github.com/documenso/documenso/assets/1309312/72d445be-41e5-4936-bdba-87ef8e70fa09">
<img style="display: block; height: 120px; width: 24%" <img style="display: block; height: 120px; width: 24%"
src="https://user-images.githubusercontent.com/1309312/224571617-1f3c2811-c1ac-4d7d-b9b0-4ab183731405.png"> src="https://github.com/documenso/documenso/assets/1309312/d7b86c0f-a755-4476-a022-a608db2c4633">
<img style="display: block; height: 120px; width: 24%" <img style="display: block; height: 120px; width: 24%"
src="https://user-images.githubusercontent.com/1309312/224570322-b2c76ea8-7482-4043-ad97-f1221220c591.png"> src=https://github.com/documenso/documenso/assets/1309312/c0f55116-ab82-433f-a266-f3fc8571d69f">
<img style="display: block; height: 120px; width: 24%"
src="https://user-images.githubusercontent.com/1309312/224570325-a8055f24-9826-4a23-b116-4fbb0577581a.png">
<img style="display: block; height: 120px; width: 24%"
src="https://user-images.githubusercontent.com/1309312/224570318-f724bbd9-c394-4bdc-bace-2d78af92de44.png">
<img style="display: block; height: 120px; width: 24%"
src="https://user-images.githubusercontent.com/1309312/224571539-f019b860-f613-4b20-86e8-4437c5784265.png">
</div> </div>
> **Note**
> This project is currently under community review and will publish it's first production release soon™.
## About this project ## About this project
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. Signing documents digitally should be fast and easy and should be the 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 ## 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> <p align="center">
<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> <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>
</p>
## Community and Next Steps 🎯 ## Community and Next Steps 🎯
We're currently working on a redesign of the application including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon. We're currently working on a redesign of the application, including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon.
- Check out the first source code release in this repository and test it - 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) - Tell us what you think in the [Discussions](https://github.com/documenso/documenso/discussions)
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members - Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members
- ⭐ the repository to help us raise awareness - ⭐ the repository to help us raise awareness
- Spread the word on Twitter, that Documenso is working towards a more open signing tool - 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 - Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
## Contributing ## Contributing
@ -84,62 +89,68 @@ Contact us if you are interested in our Enterprise plan for large organizations
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a> <a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
## Activity ## Tech Stack
![Repository Activity](https://repobeats.axiom.co/api/embed/622a2e9aa709696f7226304b5b7178a5741b3868.svg) - [Typescript](https://www.typescriptlang.org/) - Language
- [Next.js](https://nextjs.org/) - Framework
- [Prisma](https://www.prisma.io/) - ORM
- [Tailwind](https://tailwindcss.com/) - CSS
- [shadcn/ui](https://ui.shadcn.com/) - Component Library
- [NextAuth.js](https://next-auth.js.org/) - Authentication
- [react-email](https://react.email/) - Email Templates
- [tRPC](https://trpc.io/) - API
- [Node SignPDF](https://github.com/vbuch/node-signpdf) - Digital Signature
- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
- [Stripe](https://stripe.com/) - Payments
- [Vercel](https://vercel.com) - Hosting
# Tech <!-- - Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned. -->
Documenso is built using awesome open source tech including: ## Local Development
- [Typescript](https://www.typescriptlang.org/) ### Requirements
- [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/)
- [Tailwind CSS (Styling)](https://tailwindcss.com/)
- [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 `/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 To run Documenso locally you will need
## Requirements - Node.js
- Postgres SQL Database
- Docker (optional)
To run Documenso locally you need ### Developer Quickstart
- [Node.js (Version: >=18.x)](https://nodejs.org/en/download/)
- 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. > **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: 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. 1. [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh ```sh
git clone https://github.com/documenso/documenso git clone https://github.com/documenso/documenso
``` ```
- Set up your `.env` file using the recommendations in the `.env.example` file. 2. Set up your `.env` file using the recommendations in the `.env.example` file. Alternatively just run `cp .env.example .env` to get started with our handpicked defaults.
- 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 3. Run `npm run dx` in the root directory
Incoming mail will be available at http://localhost:9000 - This will spin up a postgres database and inbucket mailserver in a docker container.
Your database will also be available on port `54320`. You can connect to it using your favorite database client. 4. Run `npm run dev` in the root directory
5. Want it even faster? Just use
```sh
npm run d
```
#### Access Points for Your Application
1. **App** - http://localhost:3000
2. **Incoming Mail Access** - http://localhost:9000
3. **Database Connection Details**
- **Port**: 54320
- **Connection**: Use your favorite database client to connect using the provided port.
## Developer Setup ## Developer Setup
@ -147,33 +158,38 @@ Your database will also be available on port `54320`. You can connect to it usin
Follow these steps to setup documenso on you local machine: 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. 1. [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh
git clone https://github.com/documenso/documenso ```sh
``` git clone https://github.com/documenso/documenso
- Run `npm i` in root directory ```
- Rename `.env.example` to `.env`
- Set DATABASE_URL value in .env file 2. Run `npm i` in root directory
- You can use the provided test database url (may be wiped at any point)
- Or setup a local postgres sql instance (recommended) 3. Create your `.env` from the `.env.example`. You can use `cp .env.example .env` to get started with our handpicked defaults.
- Create the database scheme by running `db-migrate:dev`
- Setup your mail provider 4. Set the following environement variables.
- Set `SENDGRID_API_KEY` value in .env file
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/). - NEXTAUTH_URL
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the `SMTP - NEXTAUTH_SECRET
\_ - NEXT_PUBLIC_WEBAPP_URL
* variables` in your .env - NEXT_PUBLIC_MARKETING_URL
- Run `npm run dev` root directory to start - NEXT_PRIVATE_DATABASE_URL
- Register a new user at http://localhost:3000/signup - NEXT_PRIVATE_DIRECT_DATABASE_URL
- NEXT_PRIVATE_SMTP_FROM_NAME
- NEXT_PRIVATE_SMTP_FROM_ADDRESS
5. Create the database schema by running `npm run prisma:migrate-dev`
6. Run `npm run dev` root directory to start
7. Register a new user at http://localhost:3000/signup
--- ---
- Optional: Seed the database using `npm run db-seed` to create a test user and document - Optional: Seed the database using `npm run prisma:seed -w @documenso/prisma` to create a test user and document
- Optional: Upload and sign `apps/web/resources/example.pdf` manually to test your setup
- Optional: Create your own signing certificate - Optional: Create your own signing 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](./SIGNING.md)**.
- 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)**.
### Run in Gitpod ### Run in Gitpod
@ -181,81 +197,39 @@ Follow these steps to setup documenso on you local machine:
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/documenso/documenso) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/documenso/documenso)
## Updating ### Run in DevContainer
- If you pull the newest version from main, using `git pull`, it may be necessary to regenerate your database client We support DevContainers for VSCode. [Click here to get started.](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/documenso/documenso)
- You can do this by running the generate command in `/packages/prisma`:
```sh
npx prisma generate
```
- This is not necessary on first clone.
# Creating your own signing certificate ## Docker
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: 🚧 Docker containers and images are current in progress. We are actively working on bringing a simple docker build and publish pipeline for Documenso.
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key: ## Self Hosting
`openssl genrsa -out private.key 2048`
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
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 `/apps/web/resources/certificate.p12`
# 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:
- cd into `docker` directory
- Make `build.sh` executable by running `chmod +x build.sh`
- Run `./build.sh` to start building the docker image.
- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command
```
docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest
```
Command Breakdown:
- `-d` - Let's you run the container in background
- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port.
- `-v` - Volume let's you persist the data
- `--name` - Name of the container
- `documenso:latest` - Image you have built
# Deployment
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates! We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
## Railway > Please note the below deployment methods are for v0.9, we will update these to v1.0 once it has been released.
### Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/DjrRRX) [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/DjrRRX)
## Render ### Render
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso) [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso)
# Troubleshooting ## Troubleshooting
## I'm not receiving any emails when using the developer quickstart ### I'm not receiving any emails when using the developer quickstart
When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view. When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view.
The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500. The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500.
## Support IPv6 ### 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 In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the Next.js start command
For local docker run For local docker run
@ -277,5 +251,25 @@ containers:
- start - start
- -- - --
- -H - -H
- "::" - '::'
``` ```
### I can't see environment variables in my package scripts
Wrap your package script with the `with:env` script like such:
```
npm run with:env -- npm run myscript
```
The same can be done when using `npx` for one of bin scripts:
```
npm run with:env -- npx myscript
```
This will load environment variables from your `.env` and `.env.local` files.
## Repo Activity
![Repository Activity](https://repobeats.axiom.co/api/embed/622a2e9aa709696f7226304b5b7178a5741b3868.svg)

55
SIGNING.md Normal file
View File

@ -0,0 +1,55 @@
# Creating your own signing certificate
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
`openssl genrsa -out private.key 2048`
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
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 `/apps/web/resources/certificate.p12`
## 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:
- cd into `docker` directory
- Make `build.sh` executable by running `chmod +x build.sh`
- Run `./build.sh` to start building the docker image.
- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command
```
docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest
```
Command Breakdown:
- `-d` - Let's you run the container in background
- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port.
- `-v` - Volume let's you persist the data
- `--name` - Name of the container
- `documenso:latest` - Image you have built
## Deployment
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
## Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/DjrRRX)
## Render
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso)

View File

@ -1,6 +1,6 @@
--- ---
title: 'Building Documenso — Part 1: Certificates' title: 'Building Documenso — Part 1: Certificates'
description: This is the first part of the new Building Documenso series, where I describe the challenges and design choices that we make while building the worlds most open signing platform. description: In today's fast-paced world, productivity and efficiency are crucial for success, both in personal and professional endeavors. We all strive to make the most of our time and energy to achieve our goals effectively. However, it's not always easy to stay on track and maintain peak performance. In this blog post, we'll explore 10 valuable tips to help you boost productivity and efficiency in your daily life.
authorName: 'Timur Ercan' authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg' authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder' authorRole: 'Co-Founder'
@ -79,7 +79,7 @@ There werent any deeper reasons we choose WiseKey, other than they offered wh
Do you have questions or thoughts about this? As always, let me know in the comments, on <a href="http://twitter.com/eltimuro" target="_blank">twitter.com/eltimuro</a> Do you have questions or thoughts about this? As always, let me know in the comments, on <a href="http://twitter.com/eltimuro" target="_blank">twitter.com/eltimuro</a>
or directly: <a href="https://documen.so/timur" target="_blank">documen.so/timur</a> or directly: <a href="https://documen.so/timur" target="_blank">documen.so/timur</a>
Join the self-hoster community here: <a href="https://documen.so/discord" target="_blank">https://documen.so/discord</a> Join the self-hoster community here: <a href="https://documenso.slack.com/" target="_blank">https://documenso.slack.com/</a>
Best from Hamburg Best from Hamburg

View File

@ -0,0 +1,49 @@
---
title: Open Sourcing Documenso's Design
description: It's day 1 of our first launch week. We are kicking it off by open sourcing Documenso's design system! Let's go..
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2023-09-25
tags:
- Design
- Open Source
- Community
---
<figure>
<MdxNextImage
src="/blog/designsystem.png"
width="1260"
height="630"
alt="Documenso's Design System"
/>
<figcaption className="text-center">
Documenso's Design System ✨
</figcaption>
</figure>
> TLDR; Our design system is OSS under MIT at [design.documenso.com](https://design.documenso.com)
Today, we are open sourcing our design system, lovingly crafted by [Thilo](https://twitter.com/thilokonzok). The system is public on Figma and will be available at [design.documenso.com](https://design.documenso.com) from here on out.
We are publishing it under the MIT License so everyone can share, remix, and use it as it helps them most.
We chose to start our first launch week with a design topic to emphasize the role design will play in Documenso's company and community culture. As it is historically difficult to bring together open-source software with great design, this is our first step towards encouraging a more design-driven COSS (Commercial Open Source) movement.
## Designers Welcome
We added a designer role in our Discord to create a space for designers to explore and discuss design-related topics of Documenso and signing in general. In the future, we want to foster more coding contributions and start a design culture around the product. As it is much more difficult to incorporate design contributions, we have yet to find a clear plan of what that will look like. I would like to see contributions around stuff we are NOT working on. Designs in that area can inspire and start discussions without the complexities of implementing them immediately — a free-thinking space around everything Documenso. Having a free mandate to design without restriction can create many exciting ideas. Some Ideas worth exploring:
## Areas for design contributions
- Explorations of exciting aspects of signing and document handling:
- What does signing look like when we no longer have skeuomorphic signatures?
- What is signing if we move beyond paper-inspired documents?
- What would the government process look like using Documenso?
- Solutions Concepts for features further down the roadmap, e.g., Widgets in websites
These are fascinating ideas for explorative design. They won't be built 1:1 but shape how we think about signing and where it can go, which is even more critical. If you are interested in product design, you are invited to join our [Discord](https://documen.so/discord) and discuss the future of signing design or the future of Documenso's design system. Also let me know what you think on [X (formerly Twitter)](https://x.com/eltimuro).
Best from Hamburg\
Timur

View File

@ -0,0 +1,65 @@
---
title: The Early Adopters Plan
description: Launch Week Day 4 and we are still early! Early enough for you to get a sweet deal for supporting Documenso's Mission. Join the movement and get a shiny early adopter account in the process.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2023-09-28
tags:
- Paid Plan
- Metrics
- Open Startup
---
<figure>
<MdxNextImage
src="/blog/early.png"
width="1260"
height="630"
alt="XKCD: Bug"
/>
<figcaption className="text-center">
"Being early is, uh, good." -Unknown
</figcaption>
</figure>
## Community-Driven Development
As we ramp up hiring and development speed for Documenso, I want to discuss how we plan to build its core version.
As we want to be a highly community-driven product, we want to capture the thoughts and ideas of said community in two ways:
1. Everything we plan (i.e. documen.so/roadmap) and build (i.e. documen.so/repo) is public and open to comments and suggestions from everyone by design. We plan to have discussions, streams, and articles showcasing what we're up to, encouraging feedback and iterations on our plans.
2. We're looking for 100 die-hard early adopters to get more deep hands-on feedback. If you want to be among the first to use and help shape Documenso, we have a special offer for you:
## The Early Adopter Plan
- All first 100 signups are entitled to the early adopter plan.
- The plan includes everything we build in the next 12 months and unlimited<sup>1</sup> signatures
- This plan is priced at $30/mo.
- No matter what we add, the price is guaranteed for life as a thank you for supporting Documenso's mission<sup>2</sup>. And we plan to add a lot.
- This also includes unlimited users<sup>3</sup> as part of the upcoming support for teams.
- If you already claimed an early adopter account in the past and canceled, we are happy to reactivate your account with the early adopter pricing. Reach out to support@documenso.com
## Being an Early Adopter
Being eligible for the early adopter plan has no formal requirements like giving feedback or being active in the community. In good faith, we assume you sign the Documenso Supporter Pledge to be part of this. And if you want to use the newest version quietly, that's fine, too. Everything the die-hard community brings up will be carefully considered and prioritized <sup>4</sup>. Also, there will obviously be limited edition merch available for the first 100.
Documenso currently runs the community reviewed 0.9.1 version. Getting from here to the globally loved and adopted signing tool we all deserve will take a lot of work, and we want you on board to help us create it. Join us in shaping the future of open signing and having fun doing it. [Malfunction Mania](https://documenso.com/blog/malfunction-mania) and releasing 1.0 will go a long way.
## Extending our open metrics
As part of our ongoing effort to be open and transparent in our doing, we are adding "Early Adopters" to our [/open page](https://documenso.com/open) page. After we exceed the early adopter slots, this metric will transition to "Customers". When no more early adopter seats can be claimed, the early adopter plan will transition to a standard paid plan. It will still be priced at $30/mo., but will no longer include upcoming features or unlimited seats.
If you have any questions or comments, please reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord).
Best from Hamburg\
Timur
\
[1] Excluding signatures, we have to pay for i.e. third-party QES
[2] The Documenso Manifest
[3] Within reason. If you are unsure what that means, feel free to contact hi@documenso.com. But it should be fine if you don't plan to onboard a huge enterprise.
[4] We won't be able to build everything everyone asks for. But we firmly plan to listen and build in a way everyone's requirements are met as well as possible.

View File

@ -0,0 +1,57 @@
---
title: Announcing Malfunction Mania
description: Launch Week Day 2 Y'all! We're getting ready to release Documenso 1.0! Join in on the fun of making sure the open-source alternative to DocuSign is on point. We're calling a 'MALFUNCTION MANIA.'
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2023-09-26
tags:
- Testing
- Rewrite
- Bounties
---
<figure>
<MdxNextImage
src="/blog/mm.png"
width="1260"
height="630"
alt="Malfunctioning Documenso Logo in broken colors"
/>
<figcaption className="text-center">
We're calling a MALFUNCTION MANIA! 🚨
</figcaption>
</figure>
> TLDR; Documenso 1.0 is in the [staging environment](https://documen.so/staging). Go check it out.
It's been a minute since Lucas proclaimed, ["We're doing a rewrite"](https://documenso.com/blog/why-were-doing-a-rewrite), and many of you have been asking when the new version will be available. I'm happy to say that the wait has come to an end. The work on Documenso 1.0 has reached a level we feel comfortable going into the next phase with. We had a lot of community feedback, contributions, and moral support to get us this far, which is why we're excited to announce the most extensive community project yet:
As Documenso 1.0 just hit the staging environment, we're calling a MALFUNCTION MANIA. An enormous, public testing phase, where we invite everyone to try out the new version, hunt down, report, and fix any malfunctions (aka bugs), and give feedback before release. Malfunction Mania will happen alongside our internal testing, and by combining the two, we want to ensure the best possible release we can have for Version 1.0. We know many of you have been eager to contribute; this is your chance (the first of many to come).
## As part of Malfunction Mania, we're offering special bug bounties
- We award $25 - $100 per report/ issue/ fix, depending on the severity and if the problem is already known
- Bounties will be awarded for fixing reported or other critical issues via accepted Pull Requests (PR)
- Just reporting issues in a reproducible way can also be awarded
- Smaller but notable contributions like minor issues and documentation will be awarded with exclusive merch as we see fit.
## What you can do
- Head over to the [staging environment](https://documen.so/staging), check out the new version and give it a spin;
- Check out the [source code](https://github.com/documenso/documenso) on GitHub and look it over;
- Spin up the new version locally and try it out.
## How to get the bounties
- Report bugs by creating an issue here: [documen.so/issues](https://documen.so/issues);
- Fix bugs by creating a Pull Request (PR);
- Look over and add missing documentation/ Quickstarts and other useful resources.
Best from Hamburg
Timur
We don't have a specific end date for Malfunction Mania. We plan to move the staging version into the production environment by the end of the month once we're happy with the results. Bug reports and fixes are, of course, always welcome going forward.
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.**

View File

@ -12,7 +12,7 @@ tags:
Since we launched [Documenso 0.9 on Product Hunt](https://producthunt.com/products/documenso#documenso) last May, the team's been hard at work behind the scenes to ramp up development and design to deliver an excellent next version. Since we launched [Documenso 0.9 on Product Hunt](https://producthunt.com/products/documenso#documenso) last May, the team's been hard at work behind the scenes to ramp up development and design to deliver an excellent next version.
Last week, Lucas shared the reasoning on [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite). Last week, Lucas shared the reasoning how [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite).
Today, I'm pleased to share with you a preview of the next Documenso. Today, I'm pleased to share with you a preview of the next Documenso.
@ -20,11 +20,11 @@ Today, I'm pleased to share with you a preview of the next Documenso.
We redesigned the whole signing flow to make it more appealing and more convenient. We redesigned the whole signing flow to make it more appealing and more convenient.
We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it. We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it.
**We call it happy minimalism.** **We call it happy minimalism.**
We paid particular attention to the moment of signing, which should be celebrated. We paid particular attention to the moment of signing, which should be celebrated.
The image below is the final bloom of the completion celebration we added: The image below is the final bloom of the completion celebration we added:

View File

@ -0,0 +1,64 @@
---
title: Merch Mania
description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2023-09-27
tags:
- Merch
- Rewrite
- Bounties
---
<figure>
<MdxNextImage
src="/blog/mania-shirt.png"
width="1260"
height="630"
alt="Malfunctioning Mania Themed Documenso Shirt"
/>
<figcaption className="text-center">
The Limited Edition "Malfunction Mania" Shirt - Only during Malfunction Mania
</figcaption>
</figure>
> TLDR; We have a fancy limited edition shirt. Contribute to [Malfunction Mania](https://documenso.com/blog/malfunction-mania) to get one.
We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) yesterday, and the first [issues](https://github.com/documenso/documenso/issues) are coming in. As mentioned, there will be dollar bounties, but we also wanted to celebrate entering the final stage of version 1.0 with something special. This is why we created this limited edition shirt. It will only be available during the runtime of Malfunction Mania. We have yet to set an exact end date, the next event in October, however, is looming, ready to end MM.
## Documenso Merch Shop
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso.
<figure>
<MdxNextImage
src="/blog/shop.png"
width="1260"
height="630"
alt="Malfunctioning Mania Themed Documenso Shirt"
/>
<figcaption className="text-center">
Merch at Documenso is always given to those who deserve it.
</figcaption>
</figure>
## How earn the shirt
If you have been following us, you know we are not big on formalities but highly value rewarding merit. That being said, any worthwhile contribution has a chance to get one. To inspire, here are a few ideas on how to contribute to securing one:
- Report a bug with detailed reproduction details
- Fix a bug (you or somebody else reported)
- Analyze and describe a usability or user experience shortcoming
- Test the product in a systematic and least somewhat documented way
- Engage in discussion about the current version and its choices
- Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging)
- Review the version with a video, stream, or screenshots and post about it
- Review existing or create missing documentation
Best from Hamburg
Timur
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest updates about Malfunction Mania.**

View File

@ -0,0 +1,42 @@
---
title: Design System
---
# We're building a beautiful, open-source alternative to DocuSign
> Read more about our design culture here:
>
> [https://documenso.com/blog/design-system](https://documenso.com/blog/design-system)
At Documenso, we aim to be a design-driven company.
We believe that design isn't just about how things look, but also how they work. We want to make sure that the product is easy to use and intuitive. We also want to ensure that the website, desktop and mobile apps are consistent and look and feel like they belong together.
To achieve this, we've created Documenso's design system containing tokens, primitives, and components, screens, and brand assets.
We're open-sourcing this design system so you can see how we build the product and think about design as a whole.
## Check out the design system
<iframe
src="https://documen.so/design-system-embed"
className="aspect-square w-full border-none"
frameBorder="0"
/>
## Remix and Share the community version on Figma
<a href="https://documen.so/design" target="_blank">
<figure>
<MdxNextImage
src="/blog/designsystem.png"
width="1260"
height="630"
alt="Documenso's Design System"
/>
<figcaption className="text-center">
Documenso's Design System ✨
</figcaption>
</figure>
</a>

Binary file not shown.

View File

@ -2,22 +2,37 @@
const path = require('path'); const path = require('path');
const { withContentlayer } = require('next-contentlayer'); const { withContentlayer } = require('next-contentlayer');
const { parsed: env } = require('dotenv').config({ const ENV_FILES = ['.env', '.env.local', `.env.${process.env.NODE_ENV || 'development'}`];
path: path.join(__dirname, '../../.env.local'),
ENV_FILES.forEach((file) => {
require('dotenv').config({
path: path.join(__dirname, `../../${file}`),
});
}); });
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const config = { const config = {
experimental: { experimental: {
serverActions: true, serverActionsBodySizeLimit: '10mb',
}, },
reactStrictMode: true, reactStrictMode: true,
transpilePackages: ['@documenso/lib', '@documenso/prisma', '@documenso/trpc', '@documenso/ui'], transpilePackages: ['@documenso/lib', '@documenso/prisma', '@documenso/trpc', '@documenso/ui'],
env: {
NEXT_PUBLIC_PROJECT: 'marketing',
},
modularizeImports: { modularizeImports: {
'lucide-react': { 'lucide-react': {
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
}, },
}, },
webpack: (config, { isServer }) => {
// fixes: Module not found: Cant resolve ../build/Release/canvas.node
if (isServer) {
config.resolve.alias.canvas = false;
}
return config;
},
async headers() { async headers() {
return [ return [
{ {
@ -52,6 +67,14 @@ const config = {
}, },
]; ];
}, },
async rewrites() {
return [
{
source: '/ingest/:path*',
destination: 'https://eu.posthog.com/:path*',
},
];
},
}; };
module.exports = withContentlayer(config); module.exports = withContentlayer(config);

View File

@ -6,7 +6,7 @@
"scripts": { "scripts": {
"dev": "next dev -p 3001", "dev": "next dev -p 3001",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start -p 3001",
"lint": "next lint", "lint": "next lint",
"clean": "rimraf .next && rimraf node_modules", "clean": "rimraf .next && rimraf node_modules",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs" "copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
@ -21,19 +21,21 @@
"framer-motion": "^10.12.8", "framer-motion": "^10.12.8",
"lucide-react": "^0.279.0", "lucide-react": "^0.279.0",
"micro": "^10.0.1", "micro": "^10.0.1",
"next": "13.4.19", "next": "14.0.0",
"next-auth": "4.22.3", "next-auth": "4.24.3",
"next-contentlayer": "^0.3.4", "next-contentlayer": "^0.3.4",
"next-plausible": "^3.10.1", "next-plausible": "^3.10.1",
"perfect-freehand": "^1.2.0", "perfect-freehand": "^1.2.0",
"posthog-js": "^1.77.3",
"react": "18.2.0", "react": "18.2.0",
"react-confetti": "^6.1.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.43.9",
"react-icons": "^4.8.0", "react-icons": "^4.11.0",
"recharts": "^2.7.2", "recharts": "^2.7.2",
"sharp": "0.32.5", "sharp": "0.32.5",
"typescript": "5.1.6", "typescript": "5.2.2",
"zod": "^3.21.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.1.0", "@types/node": "20.1.0",

View File

@ -7,6 +7,7 @@ declare namespace NodeJS {
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string; NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string; NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
NEXT_PRIVATE_STRIPE_API_KEY: string; NEXT_PRIVATE_STRIPE_API_KEY: string;
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string; NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

@ -39,7 +39,7 @@ export default function ContentPage({ params }: { params: { content: string } })
const MDXContent = useMDXComponent(post.body.code); const MDXContent = useMDXComponent(post.body.code);
return ( return (
<article className="prose prose-slate mx-auto"> <article className="prose dark:prose-invert mx-auto">
<MDXContent components={mdxComponents} /> <MDXContent components={mdxComponents} />
</article> </article>
); );

View File

@ -1,4 +1,4 @@
import { ImageResponse } from 'next/server'; import { ImageResponse } from 'next/og';
import { allBlogPosts } from 'contentlayer/generated'; import { allBlogPosts } from 'contentlayer/generated';

View File

@ -39,21 +39,21 @@ export default function BlogPostPage({ params }: { params: { post: string } }) {
const MDXContent = useMDXComponent(post.body.code); const MDXContent = useMDXComponent(post.body.code);
return ( return (
<article className="prose prose-slate mx-auto py-8"> <article className="prose dark:prose-invert mx-auto py-8">
<div className="mb-6 text-center"> <div className="mb-6 text-center">
<time dateTime={post.date} className="mb-1 text-xs text-gray-600"> <time dateTime={post.date} className="text-muted-foreground mb-1 text-xs">
{new Date(post.date).toLocaleDateString()} {new Date(post.date).toLocaleDateString()}
</time> </time>
<h1 className="text-3xl font-bold">{post.title}</h1> <h1 className="text-3xl font-bold">{post.title}</h1>
<div className="not-prose relative -mt-2 flex items-center gap-x-4 border-b border-t py-4"> <div className="not-prose relative -mt-2 flex items-center gap-x-4 border-b border-t py-4">
<div className="h-10 w-10 rounded-full bg-gray-50"> <div className="bg-foreground h-10 w-10 rounded-full">
{post.authorImage && ( {post.authorImage && (
<img <img
src={post.authorImage} src={post.authorImage}
alt={`Image of ${post.authorName}`} alt={`Image of ${post.authorName}`}
className="h-10 w-10 rounded-full bg-gray-50" className="bg-foreground/10 h-10 w-10 rounded-full"
/> />
)} )}
</div> </div>

View File

@ -13,13 +13,13 @@ export default function BlogPage() {
<div className="text-center"> <div className="text-center">
<h1 className="text-3xl font-bold lg:text-5xl">From the blog</h1> <h1 className="text-3xl font-bold lg:text-5xl">From the blog</h1>
<p className="mx-auto mt-4 max-w-xl text-center text-lg leading-normal text-[#31373D]"> <p className="text-muted-foreground mx-auto mt-4 max-w-xl text-center text-lg leading-normal">
Get the latest news from Documenso, including product updates, team announcements and Get the latest news from Documenso, including product updates, team announcements and
more! more!
</p> </p>
</div> </div>
<div className="mt-10 divide-y divide-slate-100 border-t border-slate-200 "> <div className="divide-muted-foreground/20 border-muted-foreground/20 mt-10 divide-y border-t">
{blogPosts.map((post, i) => ( {blogPosts.map((post, i) => (
<article <article
key={`blog-${i}`} key={`blog-${i}`}
@ -57,12 +57,12 @@ export default function BlogPage() {
</div> </div>
<div className="relative mt-4 flex items-center gap-x-4"> <div className="relative mt-4 flex items-center gap-x-4">
<div className="h-10 w-10 rounded-full bg-slate-50"> <div className="bg-foreground/5 h-10 w-10 rounded-full">
{post.authorImage && ( {post.authorImage && (
<img <img
src={post.authorImage} src={post.authorImage}
alt={`Image of ${post.authorName}`} alt={`Image of ${post.authorName}`}
className="h-10 w-10 rounded-full bg-slate-50" className="bg-foreground/5 h-10 w-10 rounded-full"
/> />
)} )}
</div> </div>

View File

@ -32,10 +32,21 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
} }
const session = await stripe.checkout.sessions.retrieve(sessionId); const session = await stripe.checkout.sessions.retrieve(sessionId);
const customerId = typeof session.customer === 'string' ? session.customer : session.customer?.id;
if (!customerId) {
redirect('/');
}
const customer = await stripe.customers.retrieve(customerId);
if (!customer || customer.deleted) {
redirect('/');
}
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: { where: {
id: Number(session.client_reference_id), id: Number(customer.metadata.userId),
}, },
}); });
@ -58,40 +69,40 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
return ( return (
<div className="mt-12"> <div className="mt-12">
<h1 className="text-3xl font-bold text-slate-900 md:text-4xl"> <h1 className="text-foreground text-3xl font-bold md:text-4xl">
Welcome to the <span className="text-primary">open signing</span> revolution{' '} Welcome to the <span className="text-primary">open signing</span> revolution{' '}
<u>{user.name}</u> <u>{user.name}</u>
</h1> </h1>
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg"> <p className="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
It's not every day you get to be part of a revolution. It's not every day you get to be part of a revolution.
</p> </p>
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg"> <p className="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
But today is that day, by signing up to Documenso, you're joining a movement of people who 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. want to make the world a better place.
</p> </p>
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg"> <p className="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
We're going to change the way people sign documents. We're going to make it easier, faster, 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. and more secure. And we're going to do it together.
</p> </p>
<div className="mt-12"> <div className="mt-12">
<h2 className="text-2xl font-bold text-slate-900">Let's do it together</h2> <h2 className="text-foreground text-2xl font-bold">Let's do it together</h2>
<div className="-mx-4 mt-8 flex md:-mx-8"> <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"> <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 <p
className={cn( className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl', 'text-foreground text-4xl font-semibold md:text-5xl',
fontCaveat.className, fontCaveat.className,
)} )}
> >
Timur Timur
</p> </p>
<p className="text-sm text-slate-500 md:text-lg"> <p className="text-muted-foreground text-sm md:text-lg">
Timur Ercan Timur Ercan
<span className="block lg:hidden" /> <span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span> <span className="hidden lg:inline"> - </span>
@ -102,14 +113,14 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
<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"> <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 <p
className={cn( className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl', 'text-foreground text-4xl font-semibold md:text-5xl',
fontCaveat.className, fontCaveat.className,
)} )}
> >
Lucas Lucas
</p> </p>
<p className="text-sm text-slate-500 md:text-lg"> <p className="text-muted-foreground text-sm md:text-lg">
Lucas Smith Lucas Smith
<span className="block lg:hidden" /> <span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span> <span className="hidden lg:inline"> - </span>
@ -119,12 +130,16 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
<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"> <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 && ( {signatureDataUrl && (
<img src={signatureDataUrl} alt="your-signature" className="max-w-[172px]" /> <img
src={signatureDataUrl}
alt="your-signature"
className="max-w-[172px] dark:invert"
/>
)} )}
{!signatureDataUrl && ( {!signatureDataUrl && (
<p <p
className={cn( className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl', 'text-foreground text-4xl font-semibold md:text-5xl',
fontCaveat.className, fontCaveat.className,
)} )}
> >
@ -132,7 +147,7 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
</p> </p>
)} )}
<p className="text-sm text-slate-500 md:text-lg"> <p className="text-muted-foreground text-sm md:text-lg">
{user.name} {user.name}
<span className="block lg:hidden" /> <span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span> <span className="hidden lg:inline"> - </span>
@ -143,25 +158,25 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
</div> </div>
<div className="mt-12"> <div className="mt-12">
<h2 className="text-2xl font-bold text-slate-900">Your sign in details</h2> <h2 className="text-foreground text-2xl font-bold">Your sign in details</h2>
<div className="mt-4"> <div className="mt-4">
<p className="text-lg text-slate-500"> <p className="text-muted-foreground text-lg">
<span className="font-bold">Email:</span> {user.email} <span className="font-bold">Email:</span> {user.email}
</p> </p>
<p className="mt-2 text-lg text-slate-500"> <p className="text-muted-foreground mt-2 text-lg">
<span className="font-bold">Password:</span>{' '} <span className="font-bold">Password:</span>{' '}
<PasswordReveal password={password ?? 'password'} /> <PasswordReveal password={password ?? 'password'} />
</p> </p>
</div> </div>
<p className="mt-4 text-sm italic text-slate-500"> <p className="text-muted-foreground mt-4 text-sm italic">
This is a temporary password. Please change it as soon as possible. This is a temporary password. Please change it as soon as possible.
</p> </p>
<Link <Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/login`} href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signin`}
target="_blank" target="_blank"
className="mt-4 block" className="mt-4 block"
> >

View File

@ -1,4 +1,10 @@
import React from 'react'; 'use client';
import React, { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
import { cn } from '@documenso/ui/lib/utils';
import { Footer } from '~/components/(marketing)/footer'; import { Footer } from '~/components/(marketing)/footer';
import { Header } from '~/components/(marketing)/header'; import { Header } from '~/components/(marketing)/header';
@ -8,15 +14,36 @@ export type MarketingLayoutProps = {
}; };
export default function MarketingLayout({ children }: MarketingLayoutProps) { export default function MarketingLayout({ children }: MarketingLayoutProps) {
const [scrollY, setScrollY] = useState(0);
const pathname = usePathname();
useEffect(() => {
const onScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return ( return (
<div className="relative max-w-[100vw] overflow-y-auto overflow-x-hidden pt-20 md:pt-28"> <div
<div className="fixed left-0 top-0 z-50 w-full bg-white/50 backdrop-blur-md"> className={cn('relative max-w-[100vw] pt-20 md:pt-28', {
'overflow-y-auto overflow-x-hidden': pathname !== '/singleplayer',
})}
>
<div
className={cn('fixed left-0 top-0 z-50 w-full bg-transparent', {
'bg-background/50 backdrop-blur-md': scrollY > 5,
})}
>
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" /> <Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
</div> </div>
<div className="relative mx-auto max-w-screen-xl px-4 lg:px-8">{children}</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]" /> <Footer className="bg-background border-muted mt-24 border-t" />
</div> </div>
); );
} }

View File

@ -7,26 +7,25 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
import { formatMonth } from '@documenso/lib/client-only/format-month'; import { formatMonth } from '@documenso/lib/client-only/format-month';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { StargazersType } from './page'; export type BarMetricProps<T extends Record<string, unknown>> = HTMLAttributes<HTMLDivElement> & {
data: T;
export type MetricsDataKey = 'stars' | 'forks' | 'mergedPRs' | 'openIssues'; metricKey: keyof T[string];
export type GithubMetricProps = HTMLAttributes<HTMLDivElement> & {
data: StargazersType;
metricKey: MetricsDataKey;
title: string; title: string;
label: string; label: string;
chartHeight?: number; chartHeight?: number;
extraInfo?: JSX.Element;
}; };
export const GithubMetric = ({ export const BarMetric = <T extends Record<string, Record<keyof T[string], unknown>>>({
className, className,
data, data,
metricKey, metricKey,
title, title,
label, label,
chartHeight = 400, chartHeight = 400,
extraInfo,
...props ...props
}: GithubMetricProps) => { }: BarMetricProps<T>) => {
const formattedData = Object.keys(data) const formattedData = Object.keys(data)
.map((key) => ({ .map((key) => ({
month: formatMonth(key), month: formatMonth(key),
@ -36,7 +35,10 @@ export const GithubMetric = ({
return ( return (
<div className={cn('flex flex-col', className)} {...props}> <div className={cn('flex flex-col', className)} {...props}>
<h3 className="px-4 text-lg font-semibold">{title}</h3> <div className="flex items-center px-4">
<h3 className="text-lg font-semibold">{title}</h3>
<span>{extraInfo}</span>
</div>
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow"> <div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow">
<ResponsiveContainer width="100%" height={chartHeight}> <ResponsiveContainer width="100%" height={chartHeight}>
@ -44,13 +46,16 @@ export const GithubMetric = ({
<XAxis dataKey="month" /> <XAxis dataKey="month" />
<YAxis /> <YAxis />
<Tooltip <Tooltip
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
itemStyle={{ itemStyle={{
color: 'hsl(var(--primary-foreground))', color: 'hsl(var(--primary-foreground))',
}} }}
formatter={(value) => [Number(value), label]} formatter={(value) => [Number(value), label]}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }} cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/> />
<Bar dataKey={metricKey} fill="hsl(var(--primary))" label={label} /> <Bar dataKey={metricKey as string} fill="hsl(var(--primary))" label={label} />{' '}
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>

View File

@ -71,7 +71,7 @@ export const CapTable = ({ className, ...props }: CapTableProps) => {
</Pie> </Pie>
<Legend <Legend
formatter={(value) => { formatter={(value) => {
return <span className="text-sm text-black">{value}</span>; return <span className="text-foreground text-sm">{value}</span>;
}} }}
/> />
<Tooltip <Tooltip

View File

@ -23,22 +23,6 @@ export const TEAM_MEMBERS = [
engagement: 'Part-Time', engagement: 'Part-Time',
joinDate: 'June 6th, 2023', joinDate: 'June 6th, 2023',
}, },
{
name: 'Florent Merian',
role: 'Marketer - III',
salary: 'Project-Based',
location: 'France',
engagement: 'Full-Time',
joinDate: 'July 10th, 2023',
},
{
name: 'Thilo Konzok',
role: 'Designer',
salary: 'Project-Based',
location: 'Germany',
engagement: 'Full-Time',
joinDate: 'April 26th, 2023',
},
{ {
name: 'David Nguyen', name: 'David Nguyen',
role: 'Software Engineer - III', role: 'Software Engineer - III',
@ -47,6 +31,22 @@ export const TEAM_MEMBERS = [
engagement: 'Full-Time', engagement: 'Full-Time',
joinDate: 'July 26th, 2023', joinDate: 'July 26th, 2023',
}, },
{
name: 'Catalin-Marinel Pit',
role: 'Software Engineer - II',
salary: 80_000,
location: 'Romania',
engagement: 'Full-Time',
joinDate: 'September 4th, 2023',
},
{
name: 'Gowdhama Rajan B',
role: 'Designer - III',
salary: 100_000,
location: 'India',
engagement: 'Full-Time',
joinDate: 'October 9th, 2023',
},
]; ];
export const FUNDING_RAISED = [ export const FUNDING_RAISED = [

View File

@ -7,14 +7,14 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
import { formatMonth } from '@documenso/lib/client-only/format-month'; import { formatMonth } from '@documenso/lib/client-only/format-month';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { FUNDING_RAISED } from '~/app/(marketing)/open/data'; export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
data: Record<string, string | number>[];
};
export type FundingRaisedProps = HTMLAttributes<HTMLDivElement>; export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
const formattedData = data.map((item) => ({
export const FundingRaised = ({ className, ...props }: FundingRaisedProps) => {
const formattedData = FUNDING_RAISED.map((item) => ({
amount: Number(item.amount), amount: Number(item.amount),
date: formatMonth(item.date), date: formatMonth(item.date as string),
})); }));
return ( return (
@ -35,6 +35,9 @@ export const FundingRaised = ({ className, ...props }: FundingRaisedProps) => {
} }
/> />
<Tooltip <Tooltip
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
itemStyle={{ itemStyle={{
color: 'hsl(var(--primary-foreground))', color: 'hsl(var(--primary-foreground))',
}} }}

View File

@ -1,12 +1,14 @@
import { z } from 'zod'; import { z } from 'zod';
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
import { MetricCard } from '~/app/(marketing)/open/metric-card'; import { MetricCard } from '~/app/(marketing)/open/metric-card';
import { SalaryBands } from '~/app/(marketing)/open/salary-bands'; import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
import { BarMetric } from './bar-metrics';
import { CapTable } from './cap-table'; import { CapTable } from './cap-table';
import { FundingRaised } from './funding-raised'; import { FundingRaised } from './funding-raised';
import { GithubMetric } from './gh-metrics';
import { TeamMembers } from './team-members'; import { TeamMembers } from './team-members';
import { OpenPageTooltip } from './tooltip';
export const revalidate = 3600; export const revalidate = 3600;
@ -29,9 +31,16 @@ const ZStargazersLiveResponse = z.record(
}), }),
); );
export type StargazersType = z.infer<typeof ZStargazersLiveResponse>; const ZEarlyAdoptersResponse = z.record(
z.object({
id: z.number(),
time: z.string().datetime(),
earlyAdopters: z.number(),
}),
);
// const ZOpenPullRequestsResponse = ZMergedPullRequestsResponse; export type StargazersType = z.infer<typeof ZStargazersLiveResponse>;
export type EarlyAdoptersType = z.infer<typeof ZEarlyAdoptersResponse>;
export default async function OpenPage() { export default async function OpenPage() {
const { const {
@ -65,8 +74,16 @@ export default async function OpenPage() {
.then(async (res) => res.json()) .then(async (res) => res.json())
.then((res) => ZStargazersLiveResponse.parse(res)); .then((res) => ZStargazersLiveResponse.parse(res));
const EARLY_ADOPTERS_DATA = await fetch('https://stargrazer-live.onrender.com/api/stats/stripe', {
headers: {
accept: 'application/json',
},
})
.then(async (res) => res.json())
.then((res) => ZEarlyAdoptersResponse.parse(res));
return ( return (
<div className="mx-auto mt-12 max-w-screen-lg"> <div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1> <h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>
@ -107,10 +124,20 @@ export default async function OpenPage() {
<SalaryBands className="col-span-12 lg:col-span-6" /> <SalaryBands className="col-span-12 lg:col-span-6" />
<FundingRaised className="col-span-12 lg:col-span-6" /> <FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
<CapTable className="col-span-12 lg:col-span-6" /> <CapTable className="col-span-12 lg:col-span-6" />
<GithubMetric
<BarMetric<EarlyAdoptersType>
data={EARLY_ADOPTERS_DATA}
metricKey="earlyAdopters"
title="Early Adopters"
label="Early Adopters"
className="col-span-12 lg:col-span-6"
extraInfo={<OpenPageTooltip />}
/>
<BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="stars" metricKey="stars"
title="Github: Total Stars" title="Github: Total Stars"
@ -118,29 +145,31 @@ export default async function OpenPage() {
className="col-span-12 lg:col-span-6" className="col-span-12 lg:col-span-6"
/> />
<GithubMetric <BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="mergedPRs" metricKey="mergedPRs"
title="Github: Total Merged PRs" title="Github: Total Merged PRs"
label="Merged PRs" label="Merged PRs"
chartHeight={300} chartHeight={300}
className="col-span-12 lg:col-span-4" className="col-span-12 lg:col-span-6"
/> />
<GithubMetric
<BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="forks" metricKey="forks"
title="Github: Total Forks" title="Github: Total Forks"
label="Forks" label="Forks"
chartHeight={300} chartHeight={300}
className="col-span-12 lg:col-span-4" className="col-span-12 lg:col-span-6"
/> />
<GithubMetric
<BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="openIssues" metricKey="openIssues"
title="Github: Total Open Issues" title="Github: Total Open Issues"
label="Open Issues" label="Open Issues"
chartHeight={300} chartHeight={300}
className="col-span-12 lg:col-span-4" className="col-span-12 lg:col-span-6"
/> />
<div className="col-span-12 mt-12 flex flex-col items-center justify-center"> <div className="col-span-12 mt-12 flex flex-col items-center justify-center">

View File

@ -0,0 +1,40 @@
import React from 'react';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@documenso/ui/primitives/tooltip';
export function OpenPageTooltip() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
className="ml-2 mt-2.5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM8.24992 4.49999C8.24992 4.9142 7.91413 5.24999 7.49992 5.24999C7.08571 5.24999 6.74992 4.9142 6.74992 4.49999C6.74992 4.08577 7.08571 3.74999 7.49992 3.74999C7.91413 3.74999 8.24992 4.08577 8.24992 4.49999ZM6.00003 5.99999H6.50003H7.50003C7.77618 5.99999 8.00003 6.22384 8.00003 6.49999V9.99999H8.50003H9.00003V11H8.50003H7.50003H6.50003H6.00003V9.99999H6.50003H7.00003V6.99999H6.50003H6.00003V5.99999Z"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
></path>
</svg>
</TooltipTrigger>
<TooltipContent>
<p>
August and earlier: Active subscribers. September and beyond: Numbers of active
subscriptions.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View File

@ -0,0 +1,84 @@
'use client';
import Link from 'next/link';
import { Variants, motion } from 'framer-motion';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
import { TOSSFriendsSchema } from './schema';
const ContainerVariants: Variants = {
initial: {
opacity: 0,
},
animate: {
opacity: 1,
transition: {
staggerChildren: 0.075,
},
},
};
const CardVariants: Variants = {
initial: {
opacity: 0,
y: 50,
},
animate: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
};
const randomDegrees = () => {
const degrees = [45, 120, -140, -45];
return degrees[Math.floor(Math.random() * degrees.length)];
};
export type OSSFriendsContainerProps = {
className?: string;
ossFriends: TOSSFriendsSchema;
};
export const OSSFriendsContainer = ({ className, ossFriends }: OSSFriendsContainerProps) => {
return (
<motion.div
className={cn('grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3', className)}
variants={ContainerVariants}
initial="initial"
animate="animate"
>
{ossFriends.map((friend, index) => (
<motion.div key={index} className="h-full w-full" variants={CardVariants}>
<Card
className="h-full"
degrees={randomDegrees()}
gradient={index % 2 === 0}
spotlight={index % 2 !== 0}
>
<CardContent className="flex h-full flex-col p-6">
<CardTitle>
<Link href={friend.href}>{friend.name}</Link>
</CardTitle>
<p className="text-foreground mt-4 flex-1 text-sm">{friend.description}</p>
<div className="mt-8">
<Link target="_blank" href={friend.href}>
<Button>Learn more</Button>
</Link>
</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
);
};

View File

@ -1,152 +1,23 @@
'use client';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link';
import { Variants, motion } from 'framer-motion'; import { z } from 'zod';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
import backgroundPattern from '~/assets/background-pattern.png'; import backgroundPattern from '~/assets/background-pattern.png';
const OSSFriends = [ import { OSSFriendsContainer } from './container';
{ import { TOSSFriendsSchema, ZOSSFriendsSchema } from './schema';
name: 'BoxyHQ',
description:
'BoxyHQs suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.',
href: 'https://boxyhq.com',
},
{
name: 'Cal.com',
description:
'Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.',
href: 'https://cal.com',
},
{
name: 'Crowd.dev',
description:
'Centralize community, product, and customer data to understand which companies are engaging with your open source project.',
href: 'https://www.crowd.dev',
},
{
name: 'Documenso',
description:
'The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.',
href: 'https://documenso.com',
},
{
name: 'Erxes',
description:
'The Open-Source HubSpot Alternative. A single XOS enables to create unique and life-changing experiences that work for all types of business.',
href: 'https://erxes.io',
},
{
name: 'Formbricks',
description:
'Survey granular user segments at any point in the user journey. Gather up to 6x more insights with targeted micro-surveys. All open-source.',
href: 'https://formbricks.com',
},
{
name: 'Forward Email',
description:
'Free email forwarding for custom domains. For 6 years and counting, we are the go-to email service for thousands of creators, developers, and businesses.',
href: 'https://forwardemail.net',
},
{
name: 'GitWonk',
description:
'GitWonk is an open-source technical documentation tool, designed and built focusing on the developer experience.',
href: 'https://gitwonk.com',
},
{
name: 'Hanko',
description:
'Open-source authentication and user management for the passkey era. Integrated in minutes, for web and mobile apps.',
href: 'https://www.hanko.io',
},
{
name: 'HTMX',
description:
'HTMX is a dependency-free JavaScript library that allows you to access AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML.',
href: 'https://htmx.org',
},
{
name: 'Infisical',
description:
'Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.',
href: 'https://infisical.com',
},
{
name: 'Novu',
description:
'The open-source notification infrastructure for developers. Simple components and APIs for managing all communication channels in one place.',
href: 'https://novu.co',
},
{
name: 'OpenBB',
description:
'Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.',
href: 'https://openbb.co',
},
{
name: 'Sniffnet',
description:
'Sniffnet is a network monitoring tool to help you easily keep track of your Internet traffic.',
href: 'https://www.sniffnet.net',
},
{
name: 'Typebot',
description:
'Typebot gives you powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.',
href: 'https://typebot.io',
},
{
name: 'Webiny',
description:
'Open-source enterprise-grade serverless CMS. Own your data. Scale effortlessly. Customize everything.',
href: 'https://www.webiny.com',
},
{
name: 'Webstudio',
description: 'Webstudio is an open source alternative to Webflow',
href: 'https://webstudio.is',
},
];
const ContainerVariants: Variants = { export default async function OSSFriendsPage() {
initial: { const ossFriends: TOSSFriendsSchema = await fetch('https://formbricks.com/api/oss-friends', {
opacity: 0, next: {
}, revalidate: 3600,
animate: {
opacity: 1,
transition: {
staggerChildren: 0.075,
}, },
}, })
}; .then(async (res) => res.json())
.then(async (data) => z.object({ data: ZOSSFriendsSchema }).parseAsync(data))
.then(({ data }) => data)
.catch(() => []);
const CardVariants: Variants = {
initial: {
opacity: 0,
y: 50,
},
animate: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
};
const randomDegrees = () => {
const degrees = [45, 120, -140, -45];
return degrees[Math.floor(Math.random() * degrees.length)];
};
export default function OSSFriendsPage() {
return ( return (
<div className="relative mt-12"> <div className="relative mt-12">
<div className="text-center"> <div className="text-center">
@ -154,49 +25,19 @@ export default function OSSFriendsPage() {
Our <span title="Open Source Software">OSS</span> Friends Our <span title="Open Source Software">OSS</span> Friends
</h1> </h1>
<p className="mx-auto mt-4 max-w-[55ch] text-lg leading-normal text-[#31373D]"> <p className="text-foreground mx-auto mt-4 max-w-[55ch] text-lg leading-normal">
We love open source and so should you, below you can find a list of our friends who are We love open source and so should you, below you can find a list of our friends who are
just as passionate about open source as we are. just as passionate about open source as we are.
</p> </p>
</div> </div>
<motion.div <OSSFriendsContainer className="mt-12" ossFriends={ossFriends} />
className="mt-12 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3"
variants={ContainerVariants}
initial="initial"
animate="animate"
>
{OSSFriends.map((friend, index) => (
<motion.div key={index} className="h-full w-full" variants={CardVariants}>
<Card
className="h-full"
degrees={randomDegrees()}
gradient={index % 2 === 0}
spotlight={index % 2 !== 0}
>
<CardContent className="flex h-full flex-col p-6">
<CardTitle>
<Link href={friend.href}>{friend.name}</Link>
</CardTitle>
<p className="mt-4 flex-1 text-sm text-slate-700">{friend.description}</p>
<div className="mt-8">
<Link target="_blank" href={friend.href}>
<Button>Learn more</Button>
</Link>
</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
<div className="absolute inset-0 -z-10 flex items-start justify-center"> <div className="absolute inset-0 -z-10 flex items-start justify-center">
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="-mr-[15vw] -mt-[15vh] h-full max-h-[150vh] scale-125 object-cover md:-mr-[50vw] md:scale-150 lg:scale-[175%]" className="-mr-[15vw] -mt-[15vh] h-full max-h-[150vh] scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:-mr-[50vw] md:scale-150 lg:scale-[175%]"
/> />
</div> </div>
</div> </div>

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZOSSFriendsSchema = z.array(
z.object({
name: z.string(),
href: z.string().url(),
description: z.string(),
}),
);
export type TOSSFriendsSchema = z.infer<typeof ZOSSFriendsSchema>;

View File

@ -6,6 +6,7 @@ import {
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from '@documenso/ui/primitives/accordion'; } from '@documenso/ui/primitives/accordion';
import { Button } from '@documenso/ui/primitives/button';
import { PricingTable } from '~/components/(marketing)/pricing-table'; import { PricingTable } from '~/components/(marketing)/pricing-table';
@ -20,20 +21,40 @@ export type PricingPageProps = {
export default function PricingPage() { export default function PricingPage() {
return ( return (
<div className="mt-12"> <div className="mt-6 sm:mt-12">
<div className="text-center"> <div className="text-center">
<h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1> <h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1>
<p className="mt-4 text-lg leading-normal text-[#31373D]"> <p className="text-foreground mt-4 text-lg leading-normal">
Designed for every stage of your journey. Designed for every stage of your journey.
</p> </p>
<p className="text-lg leading-normal text-[#31373D]">Get started today.</p> <p className="text-foreground text-lg leading-normal">Get started today.</p>
</div> </div>
<div className="mt-12"> <div className="mt-12">
<PricingTable /> <PricingTable />
</div> </div>
<div className="mx-auto mt-36 max-w-2xl">
<h2 className="text-center text-2xl font-semibold">
None of these work for you? Try self-hosting!
</h2>
<p className="text-muted-foreground mt-4 text-center leading-relaxed">
Our self-hosted option is great for small teams and individuals who need a simple
solution. You can use our docker based setup to get started in minutes. Take control with
full customizability and data ownership.
</p>
<div className="mt-4 flex justify-center">
<Button variant="outline" size="lg" className="rounded-full hover:cursor-pointer" asChild>
<Link href="https://github.com/documenso/documenso" target="_blank">
Get Started
</Link>
</Button>
</div>
</div>
<div className="mx-auto mt-36 max-w-2xl"> <div className="mx-auto mt-36 max-w-2xl">
{/* FAQ Section */} {/* FAQ Section */}
@ -45,12 +66,12 @@ export default function PricingPage() {
What is the difference between the plans? What is the difference between the plans?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
You can self-host Documenso for free or use our ready-to-use hosted version. The 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 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 adopters will get access to all features we build this year, for no additional cost!
no additional cost! Forever! Yes, that includes multiple users per account later. If Forever! Yes, that includes multiple users per account later. If you want Documenso
you want Documenso for your enterprise, we are happy to talk about your needs. for your enterprise, we are happy to talk about your needs.
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
@ -59,7 +80,7 @@ export default function PricingPage() {
How do you handle my data? How do you handle my data?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
Securely. Our data centers are located in Frankfurt (Germany), giving us the best 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 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. best practices to ensure the security and integrity of the data entrusted to us.
@ -71,7 +92,7 @@ export default function PricingPage() {
Why should I use your hosting service? Why should I use your hosting service?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
Using our hosted version is the easiest way to get started, you can simply subscribe 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 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 on your business. Additionally, when using our hosted version you benefit from our
@ -84,7 +105,7 @@ export default function PricingPage() {
How can I contribute? How can I contribute?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
That's awesome. You can take a look at the current{' '} That's awesome. You can take a look at the current{' '}
<Link <Link
className="text-documenso-700 font-bold" className="text-documenso-700 font-bold"
@ -111,7 +132,7 @@ export default function PricingPage() {
Can I use Documenso commercially? Can I use Documenso commercially?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
Yes! Documenso is offered under the GNU AGPL V3 open source license. This means you 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 can use it for free and even modify it to fit your needs, as long as you publish your
changes under the same license. changes under the same license.
@ -123,7 +144,7 @@ export default function PricingPage() {
Why should I prefer Documenso over DocuSign or some other signing tool? Why should I prefer Documenso over DocuSign or some other signing tool?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
Documenso is a community effort to create an open and vibrant ecosystem around a tool, 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 everybody is free to use and adapt. By being truly open we want to create trusted
infrastructure for the future of the internet. infrastructure for the future of the internet.
@ -135,7 +156,7 @@ export default function PricingPage() {
Where can I get support? Where can I get support?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
We are happy to assist you at{' '} We are happy to assist you at{' '}
<Link <Link
className="text-documenso-700 font-bold" className="text-documenso-700 font-bold"

View File

@ -0,0 +1,33 @@
import { notFound } from 'next/navigation';
import { getDocumentAndRecipientByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { DocumentStatus } from '@documenso/prisma/client';
import { SinglePlayerModeSuccess } from '~/components/(marketing)/single-player-mode/single-player-mode-success';
export type SinglePlayerModeSuccessPageProps = {
params: {
token?: string;
};
};
export default async function SinglePlayerModeSuccessPage({
params: { token },
}: SinglePlayerModeSuccessPageProps) {
if (!token) {
return notFound();
}
const document = await getDocumentAndRecipientByToken({
token,
}).catch(() => null);
if (!document || document.status !== DocumentStatus.COMPLETED) {
return notFound();
}
const signatures = await getRecipientSignatures({ recipientId: document.Recipient.id });
return <SinglePlayerModeSuccess document={document} signatures={signatures} />;
}

View File

@ -0,0 +1,262 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { base64 } from '@documenso/lib/universal/base64';
import { putFile } from '@documenso/lib/universal/upload/put-file';
import { DocumentDataType, Field, Prisma, Recipient } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature';
import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
import {
DocumentFlowFormContainer,
DocumentFlowFormContainerHeader,
} from '@documenso/ui/primitives/document-flow/document-flow-root';
import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { createSinglePlayerDocument } from '~/components/(marketing)/single-player-mode/create-single-player-document.action';
type SinglePlayerModeStep = 'fields' | 'sign';
// !: This entire file is a hack to get around failed prerendering of
// !: the Single Player Mode page. This regression was introduced during
// !: the upgrade of Next.js to v13.5.x.
export const SinglePlayerClient = () => {
const analytics = useAnalytics();
const router = useRouter();
const { toast } = useToast();
const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>();
const [step, setStep] = useState<SinglePlayerModeStep>('fields');
const [fields, setFields] = useState<Field[]>([]);
const documentFlow: Record<SinglePlayerModeStep, DocumentFlowStep> = {
fields: {
title: 'Add document',
description: 'Upload a document and add fields.',
stepIndex: 1,
onBackStep: uploadedFile
? () => {
setUploadedFile(null);
setFields([]);
}
: undefined,
onNextStep: () => setStep('sign'),
},
sign: {
title: 'Sign',
description: 'Enter your details.',
stepIndex: 2,
onBackStep: () => setStep('fields'),
},
};
const currentDocumentFlow = documentFlow[step];
useEffect(() => {
analytics.startSessionRecording('marketing_session_recording_spm');
return () => {
analytics.stopSessionRecording();
};
}, [analytics]);
/**
* Insert the selected fields into the local state.
*/
const onFieldsSubmit = (data: TAddFieldsFormSchema) => {
if (!uploadedFile) {
return;
}
setFields(
data.fields.map((field, i) => ({
id: i,
documentId: -1,
recipientId: -1,
type: field.type,
page: field.pageNumber,
positionX: new Prisma.Decimal(field.pageX),
positionY: new Prisma.Decimal(field.pageY),
width: new Prisma.Decimal(field.pageWidth),
height: new Prisma.Decimal(field.pageHeight),
customText: '',
inserted: false,
})),
);
analytics.capture('Marketing: SPM - Fields added');
documentFlow.fields.onNextStep?.();
};
/**
* Upload, create, sign and send the document.
*/
const onSignSubmit = async (data: TAddSignatureFormSchema) => {
if (!uploadedFile) {
return;
}
try {
const putFileData = await putFile(uploadedFile.file);
const documentToken = await createSinglePlayerDocument({
documentData: {
type: putFileData.type,
data: putFileData.data,
},
documentName: uploadedFile.file.name,
signer: data,
fields: fields.map((field) => ({
page: field.page,
type: field.type,
positionX: field.positionX.toNumber(),
positionY: field.positionY.toNumber(),
width: field.width.toNumber(),
height: field.height.toNumber(),
})),
});
analytics.capture('Marketing: SPM - Document signed', {
signer: data.email,
});
router.push(`/singleplayer/${documentToken}/success`);
} catch {
toast({
title: 'Something went wrong',
description: 'Please try again later.',
variant: 'destructive',
});
}
};
const placeholderRecipient: Recipient = {
id: -1,
documentId: -1,
email: '',
name: '',
token: '',
expired: null,
signedAt: null,
readStatus: 'OPENED',
signingStatus: 'NOT_SIGNED',
sendStatus: 'NOT_SENT',
};
const onFileDrop = async (file: File) => {
try {
const arrayBuffer = await file.arrayBuffer();
const fileBase64 = base64.encode(new Uint8Array(arrayBuffer));
setUploadedFile({
file,
fileBase64,
});
analytics.capture('Marketing: SPM - Document uploaded');
} catch {
toast({
title: 'Something went wrong',
description: 'Please try again later.',
variant: 'destructive',
});
}
};
return (
<div className="mt-6 sm:mt-12">
<div className="text-center">
<h1 className="text-3xl font-bold lg:text-5xl">Single Player Mode</h1>
<p className="text-foreground mx-auto mt-4 max-w-[50ch] text-lg leading-normal">
Create a{' '}
<Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
target="_blank"
className="hover:text-foreground/80 font-semibold transition-colors"
>
free account
</Link>{' '}
or view our{' '}
<Link
href={'/pricing'}
target="_blank"
className="hover:text-foreground/80 font-semibold transition-colors"
>
community plan
</Link>{' '}
for exclusive features, including the ability to collaborate with multiple signers.
</p>
</div>
<div className="mt-12 grid w-full grid-cols-12 gap-8">
<div className="col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7">
{uploadedFile ? (
<Card gradient>
<CardContent className="p-2">
<LazyPDFViewer
documentData={{
id: '',
data: uploadedFile.fileBase64,
initialData: uploadedFile.fileBase64,
type: DocumentDataType.BYTES_64,
}}
/>
</CardContent>
</Card>
) : (
<DocumentDropzone className="h-[80vh] max-h-[60rem]" onDrop={onFileDrop} />
)}
</div>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
<DocumentFlowFormContainer className="top-24" onSubmit={(e) => e.preventDefault()}>
<DocumentFlowFormContainerHeader
title={currentDocumentFlow.title}
description={currentDocumentFlow.description}
/>
{/* Add fields to PDF page. */}
{step === 'fields' && (
<fieldset disabled={!uploadedFile} className="flex h-full flex-col">
<AddFieldsFormPartial
documentFlow={documentFlow.fields}
hideRecipients={true}
recipients={uploadedFile ? [placeholderRecipient] : []}
numberOfSteps={Object.keys(documentFlow).length}
fields={fields}
onSubmit={onFieldsSubmit}
/>
</fieldset>
)}
{/* Enter user details and signature. */}
{step === 'sign' && (
<AddSignatureFormPartial
documentFlow={documentFlow.sign}
numberOfSteps={Object.keys(documentFlow).length}
fields={fields}
onSubmit={onSignSubmit}
requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
/>
)}
</DocumentFlowFormContainer>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,10 @@
import { SinglePlayerClient } from './client';
export const revalidate = 0;
// !: This entire file is a hack to get around failed prerendering of
// !: the Single Player Mode page. This regression was introduced during
// !: the upgrade of Next.js to v13.5.x.
export default function SingleplayerPage() {
return <SinglePlayerClient />;
}

View File

@ -1,12 +1,21 @@
import { Inter } from 'next/font/google'; import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster'; import { Toaster } from '@documenso/ui/primitives/toaster';
import { ThemeProvider } from '~/providers/next-theme';
import { PlausibleProvider } from '~/providers/plausible'; import { PlausibleProvider } from '~/providers/plausible';
import { PostHogPageview } from '~/providers/posthog';
import './globals.css'; import './globals.css';
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' }); const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
export const metadata = { export const metadata = {
title: 'Documenso - The Open Source DocuSign Alternative', title: 'Documenso - The Open Source DocuSign Alternative',
@ -32,9 +41,15 @@ export const metadata = {
}, },
}; };
export default function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getAllAnonymousFlags();
return ( return (
<html lang="en" className={fontInter.variable} suppressHydrationWarning> <html
lang="en"
className={cn(fontInter.variable, fontCaveat.variable)}
suppressHydrationWarning
>
<head> <head>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <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="32x32" href="/favicon-32x32.png" />
@ -42,8 +57,19 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
</head> </head>
<Suspense>
<PostHogPageview />
</Suspense>
<body> <body>
<PlausibleProvider>{children}</PlausibleProvider> <FeatureFlagProvider initialFlags={flags}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<PlausibleProvider>
<TrpcProvider>{children}</TrpcProvider>
</PlausibleProvider>
</ThemeProvider>
</FeatureFlagProvider>
<Toaster /> <Toaster />
</body> </body>
</html> </html>

View File

@ -26,7 +26,7 @@ export default function NotFound() {
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover md:scale-100 lg:scale-[100%]" className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-100 lg:scale-[100%]"
priority priority
/> />
</motion.div> </motion.div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

View File

@ -2,8 +2,8 @@
import Link from 'next/link'; import Link from 'next/link';
import { Github } from 'lucide-react';
import { usePlausible } from 'next-plausible'; import { usePlausible } from 'next-plausible';
import { LuGithub } from 'react-icons/lu';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -40,8 +40,8 @@ export const Callout = ({ starCount }: CalloutProps) => {
className="rounded-full bg-transparent backdrop-blur-sm" className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick} onClick={onSignUpClick}
> >
Get the Community Plan Get the Early Adopters Plan
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs"> <span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever! $30/mo. forever!
</span> </span>
</Button> </Button>
@ -52,10 +52,10 @@ export const Callout = ({ starCount }: CalloutProps) => {
onClick={() => event('view-github')} onClick={() => event('view-github')}
> >
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm"> <Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
<Github className="mr-2 h-5 w-5" /> <LuGithub className="mr-2 h-5 w-5" />
Star on Github Star on Github
{starCount && starCount > 0 && ( {starCount && starCount > 0 && (
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs"> <span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
{starCount.toLocaleString('en-US')} {starCount.toLocaleString('en-US')}
</span> </span>
)} )}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
@ -10,6 +10,7 @@ import { usePlausible } from 'next-plausible';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -29,7 +30,7 @@ import { claimPlan } from '~/api/claim-plan/fetcher';
import { FormErrorMessage } from '../form/form-error-message'; import { FormErrorMessage } from '../form/form-error-message';
export const ZClaimPlanDialogFormSchema = z.object({ export const ZClaimPlanDialogFormSchema = z.object({
name: z.string().min(3), name: z.string().trim().min(3, { message: 'Please enter a valid name.' }),
email: z.string().email(), email: z.string().email(),
}); });
@ -43,17 +44,19 @@ export type ClaimPlanDialogProps = {
export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => { export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => {
const params = useSearchParams(); const params = useSearchParams();
const { toast } = useToast(); const analytics = useAnalytics();
const event = usePlausible(); const event = usePlausible();
const { toast } = useToast();
const [open, setOpen] = useState(() => params?.get('cancelled') === 'true'); const [open, setOpen] = useState(() => params?.get('cancelled') === 'true');
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
reset,
} = useForm<TClaimPlanDialogFormSchema>({ } = useForm<TClaimPlanDialogFormSchema>({
mode: 'onBlur',
defaultValues: { defaultValues: {
name: params?.get('name') ?? '', name: params?.get('name') ?? '',
email: params?.get('email') ?? '', email: params?.get('email') ?? '',
@ -73,10 +76,12 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
]); ]);
event('claim-plan-pricing'); event('claim-plan-pricing');
analytics.capture('Marketing: Claim plan', { planId, email });
window.location.href = redirectUrl; window.location.href = redirectUrl;
} catch (error) { } catch (error) {
event('claim-plan-failed'); event('claim-plan-failed');
analytics.capture('Marketing: Claim plan failure', { planId, email });
toast({ toast({
title: 'Something went wrong', title: 'Something went wrong',
@ -86,6 +91,12 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
} }
}; };
useEffect(() => {
if (!isSubmitting && !open) {
reset();
}
}, [open]);
return ( return (
<Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}> <Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}>
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
@ -118,7 +129,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
)} )}
<div> <div>
<Label className="text-slate-500">Name</Label> <Label className="text-muted-foreground">Name</Label>
<Input type="text" className="mt-2" {...register('name')} autoFocus /> <Input type="text" className="mt-2" {...register('name')} autoFocus />
@ -126,7 +137,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
</div> </div>
<div> <div>
<Label className="text-slate-500">Email</Label> <Label className="text-muted-foreground">Email</Label>
<Input type="email" className="mt-2" {...register('email')} /> <Input type="email" className="mt-2" {...register('email')} />
@ -134,7 +145,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
</div> </div>
<Button type="submit" size="lg" loading={isSubmitting}> <Button type="submit" size="lg" loading={isSubmitting}>
Claim the Community Plan ( Claim the early adopters Plan (
{/* eslint-disable-next-line turbo/no-undeclared-env-vars */} {/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID {planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
? 'Monthly' ? 'Monthly'

View File

@ -0,0 +1,46 @@
'use client';
import React, { useEffect, useState } from 'react';
import Confetti from 'react-confetti';
import { createPortal } from 'react-dom';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { useWindowSize } from '@documenso/lib/client-only/hooks/use-window-size';
export const ConfettiScreen = ({
numberOfPieces: numberOfPiecesProp = 200,
...props
}: React.ComponentPropsWithoutRef<typeof Confetti> & { duration?: number }) => {
const isMounted = useIsMounted();
const { width, height } = useWindowSize();
const [numberOfPieces, setNumberOfPieces] = useState(numberOfPiecesProp);
useEffect(() => {
if (!props.duration) {
return;
}
const timer = setTimeout(() => {
setNumberOfPieces(0);
}, props.duration);
return () => clearTimeout(timer);
}, [props.duration]);
if (!isMounted) {
return null;
}
return createPortal(
<Confetti
{...props}
className="w-full"
numberOfPieces={numberOfPieces}
width={width}
height={height}
/>,
document.body,
);
};

View File

@ -22,7 +22,7 @@ export const FasterSmarterBeautifulBento = ({
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]" className="h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
/> />
</div> </div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24"> <h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
@ -33,41 +33,53 @@ export const FasterSmarterBeautifulBento = ({
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8"> <div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2" degrees={45} gradient> <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]"> <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"> <p className="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
<strong className="block">Fast.</strong> <strong className="block">Fast.</strong>
When it comes to sending or receiving a contract, you can count on lightning-fast When it comes to sending or receiving a contract, you can count on lightning-fast
speeds. speeds.
</p> </p>
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6"> <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" /> <Image
src={cardFastFigure}
alt="its fast"
className="max-w-[80%] dark:contrast-[70%] dark:hue-rotate-180 dark:invert lg:max-w-none"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Beautiful.</strong> <strong className="block">Beautiful.</strong>
Because signing should be celebrated. Thats why we care about the smallest detail in Because signing should be celebrated. Thats why we care about the smallest detail in
our product. our product.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardBeautifulFigure} alt="its fast" className="w-full max-w-xs" /> <Image
src={cardBeautifulFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Smart.</strong> <strong className="block">Smart.</strong>
Our custom templates come with smart rules that can help you save time and energy. Our custom templates come with smart rules that can help you save time and energy.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardSmartFigure} alt="its fast" className="w-full max-w-[16rem]" /> <Image
src={cardSmartFigure}
alt="its fast"
className="w-full max-w-[16rem] dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,65 +1,98 @@
'use client';
import { HTMLAttributes } from 'react'; import { HTMLAttributes } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { Github, MessagesSquare, Twitter } from 'lucide-react'; import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { FaXTwitter } from 'react-icons/fa6';
import { LiaDiscord } from 'react-icons/lia';
import { LuGithub } from 'react-icons/lu';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
export type FooterProps = HTMLAttributes<HTMLDivElement>; export type FooterProps = HTMLAttributes<HTMLDivElement>;
const SOCIAL_LINKS = [ const SOCIAL_LINKS = [
{ href: 'https://twitter.com/documenso', icon: <Twitter className="h-6 w-6" /> }, { href: 'https://twitter.com/documenso', icon: <FaXTwitter className="h-6 w-6" /> },
{ href: 'https://github.com/documenso/documenso', icon: <Github className="h-6 w-6" /> }, { href: 'https://github.com/documenso/documenso', icon: <LuGithub className="h-6 w-6" /> },
{ href: 'https://documen.so/discord', icon: <MessagesSquare className="h-6 w-6" /> }, { href: 'https://documen.so/discord', icon: <LiaDiscord className="h-7 w-7" /> },
]; ];
const FOOTER_LINKS = [ const FOOTER_LINKS = [
{ href: '/pricing', text: 'Pricing' }, { href: '/pricing', text: 'Pricing' },
{ href: '/singleplayer', text: 'Singleplayer' },
{ href: '/blog', text: 'Blog' }, { href: '/blog', text: 'Blog' },
{ href: '/design-system', text: 'Design' },
{ href: '/open', text: 'Open' }, { href: '/open', text: 'Open' },
{ href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' }, { href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' },
{ href: 'https://status.documenso.com', text: 'Status', target: '_blank' }, { href: 'https://status.documenso.com', text: 'Status', target: '_blank' },
{ href: 'mailto:support@documenso.com', text: 'Support' }, { href: 'mailto:support@documenso.com', text: 'Support', target: '_blank' },
{ href: '/privacy', text: 'Privacy' }, { href: '/privacy', text: 'Privacy' },
]; ];
export const Footer = ({ className, ...props }: FooterProps) => { export const Footer = ({ className, ...props }: FooterProps) => {
const { setTheme } = useTheme();
return ( return (
<div className={cn('border-t py-12', className)} {...props}> <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 className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8">
<div> <div>
<Link href="/"> <Link href="/">
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image> <Image
src="/logo.png"
alt="Documenso Logo"
className="dark:invert"
width={170}
height={0}
/>
</Link> </Link>
<div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4 text-[#8D8D8D]"> <div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4">
{SOCIAL_LINKS.map((link, index) => ( {SOCIAL_LINKS.map((link, index) => (
<Link key={index} href={link.href} target="_blank" className="hover:text-[#6D6D6D]"> <Link
key={index}
href={link.href}
target="_blank"
className="text-muted-foreground hover:text-muted-foreground/80"
>
{link.icon} {link.icon}
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2.5"> <div className="grid max-w-xs flex-1 grid-cols-2 gap-x-4 gap-y-2">
{FOOTER_LINKS.map((link, index) => ( {FOOTER_LINKS.map((link, index) => (
<Link <Link
key={index} key={index}
href={link.href} href={link.href}
target={link.target} target={link.target}
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 text-sm"
> >
{link.text} {link.text}
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
<div className="mx-auto mt-4 w-full max-w-screen-xl px-8 md:mt-12 lg:mt-24"> <div className="mx-auto mt-4 flex w-full max-w-screen-xl flex-wrap justify-between gap-4 px-8 md:mt-12 lg:mt-24">
<p className="text-sm text-[#8D8D8D]"> <p className="text-muted-foreground text-sm">
© {new Date().getFullYear()} Documenso, Inc. All rights reserved. © {new Date().getFullYear()} Documenso, Inc. All rights reserved.
</p> </p>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2.5">
<button type="button" className="text-muted-foreground" onClick={() => setTheme('light')}>
<Sun className="h-5 w-5" />
<span className="sr-only">Light</span>
</button>
<button type="button" className="text-muted-foreground" onClick={() => setTheme('dark')}>
<Moon className="h-5 w-5" />
<span className="sr-only">Dark</span>
</button>
</div>
</div> </div>
</div> </div>
); );

View File

@ -5,6 +5,7 @@ import { HTMLAttributes, useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { HamburgerMenu } from './mobile-hamburger'; import { HamburgerMenu } from './mobile-hamburger';
@ -15,29 +16,59 @@ export type HeaderProps = HTMLAttributes<HTMLElement>;
export const Header = ({ className, ...props }: HeaderProps) => { export const Header = ({ className, ...props }: HeaderProps) => {
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false); const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
const { getFlag } = useFeatureFlags();
const isSinglePlayerModeMarketingEnabled = getFlag('marketing_header_single_player_mode');
return ( return (
<header className={cn('flex items-center justify-between', className)} {...props}> <header className={cn('flex items-center justify-between', className)} {...props}>
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}> <div className="flex items-center space-x-4">
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} /> <Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
</Link> <Image
src="/logo.png"
alt="Documenso Logo"
className="dark:invert"
width={170}
height={25}
/>
</Link>
{isSinglePlayerModeMarketingEnabled && (
<Link
href="/singleplayer"
className="bg-primary dark:text-background rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
>
Try now!
</Link>
)}
</div>
<div className="hidden items-center gap-x-6 md:flex"> <div className="hidden items-center gap-x-6 md:flex">
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"> <Link
href="/pricing"
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
>
Pricing Pricing
</Link> </Link>
<Link href="/blog" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"> <Link
href="/blog"
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
>
Blog Blog
</Link> </Link>
<Link href="/open" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"> <Link
href="/open"
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
>
Open Open
</Link> </Link>
<Link <Link
href="https://app.documenso.com/login" href="https://app.documenso.com/signin"
target="_blank" target="_blank"
className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
> >
Sign in Sign in
</Link> </Link>

View File

@ -4,9 +4,11 @@ import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { Variants, motion } from 'framer-motion'; import { Variants, motion } from 'framer-motion';
import { Github } from 'lucide-react';
import { usePlausible } from 'next-plausible'; import { usePlausible } from 'next-plausible';
import { LuGithub } from 'react-icons/lu';
import { match } from 'ts-pattern';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -51,6 +53,10 @@ const HeroTitleVariants: Variants = {
export const Hero = ({ className, ...props }: HeroProps) => { export const Hero = ({ className, ...props }: HeroProps) => {
const event = usePlausible(); const event = usePlausible();
const { getFlag } = useFeatureFlags();
const heroMarketingCTA = getFlag('marketing_landing_hero_cta');
const onSignUpClick = () => { const onSignUpClick = () => {
const el = document.getElementById('email'); const el = document.getElementById('email');
@ -80,7 +86,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="-mr-[50vw] -mt-[15vh] h-full scale-125 object-cover md:scale-150 lg:scale-[175%]" className="-mr-[50vw] -mt-[15vh] h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
/> />
</motion.div> </motion.div>
</div> </div>
@ -108,37 +114,59 @@ export const Hero = ({ className, ...props }: HeroProps) => {
className="rounded-full bg-transparent backdrop-blur-sm" className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick} onClick={onSignUpClick}
> >
Get the Community Plan Get the Early Adopters Plan
<span className="bg-primary -mr-2 ml-2.5 rounded-full px-2 py-1.5 text-xs"> <span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever! $30/mo. forever!
</span> </span>
</Button> </Button>
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}> <Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm"> <Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
<Github className="mr-2 h-5 w-5" /> <LuGithub className="mr-2 h-5 w-5" />
Star on Github Star on Github
</Button> </Button>
</Link> </Link>
</motion.div> </motion.div>
<motion.div {match(heroMarketingCTA)
variants={HeroTitleVariants} .with('spm', () => (
initial="initial" <motion.div
animate="animate" variants={HeroTitleVariants}
className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-4" initial="initial"
> animate="animate"
<Link className="border-primary bg-background hover:bg-muted mx-auto mt-8 w-60 rounded-xl border transition-colors duration-300"
href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso" >
target="_blank" <Link href="/singleplayer" className="block px-4 py-2 text-center">
> <h2 className="text-muted-foreground text-xs font-semibold">
<img Introducing Single Player Mode
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily" </h2>
alt="Documenso - The open source DocuSign alternative | Product Hunt"
style={{ width: '250px', height: '54px' }} <h1 className="text-foreground mt-1.5 font-medium leading-5">
/> Self sign for free!
</Link> </h1>
</motion.div> </Link>
</motion.div>
))
.with('productHunt', () => (
<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>
))
.otherwise(() => null)}
<motion.div <motion.div
className="mt-12" className="mt-12"

View File

@ -4,7 +4,9 @@ import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { motion, useReducedMotion } from 'framer-motion'; import { motion, useReducedMotion } from 'framer-motion';
import { Github, MessagesSquare, Twitter } from 'lucide-react'; import { FaXTwitter } from 'react-icons/fa6';
import { LiaDiscord } from 'react-icons/lia';
import { LuGithub } from 'react-icons/lu';
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet'; import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
@ -14,6 +16,10 @@ export type MobileNavigationProps = {
}; };
export const MENU_NAVIGATION_LINKS = [ export const MENU_NAVIGATION_LINKS = [
{
href: '/singleplayer',
text: 'Singleplayer',
},
{ {
href: '/blog', href: '/blog',
text: 'Blog', text: 'Blog',
@ -33,13 +39,14 @@ export const MENU_NAVIGATION_LINKS = [
{ {
href: 'mailto:support@documenso.com', href: 'mailto:support@documenso.com',
text: 'Support', text: 'Support',
target: '_blank',
}, },
{ {
href: '/privacy', href: '/privacy',
text: 'Privacy', text: 'Privacy',
}, },
{ {
href: 'https://app.documenso.com/login', href: 'https://app.documenso.com/signin',
text: 'Sign in', text: 'Sign in',
}, },
]; ];
@ -55,7 +62,13 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}> <Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
<SheetContent className="w-full max-w-[400px]"> <SheetContent className="w-full max-w-[400px]">
<Link href="/" className="z-10" onClick={handleMenuItemClick}> <Link href="/" className="z-10" onClick={handleMenuItemClick}>
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} /> <Image
src="/logo.png"
alt="Documenso Logo"
className="dark:invert"
width={170}
height={25}
/>
</Link> </Link>
<motion.div <motion.div
@ -66,7 +79,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
staggerChildren: 0.03, staggerChildren: 0.03,
}} }}
> >
{MENU_NAVIGATION_LINKS.map(({ href, text }) => ( {MENU_NAVIGATION_LINKS.map(({ href, text, target }) => (
<motion.div <motion.div
key={href} key={href}
variants={{ variants={{
@ -85,9 +98,10 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
}} }}
> >
<Link <Link
className="text-2xl font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
href={href} href={href}
onClick={() => handleMenuItemClick()} onClick={() => handleMenuItemClick()}
target={target}
> >
{text} {text}
</Link> </Link>
@ -99,25 +113,25 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Link <Link
href="https://twitter.com/documenso" href="https://twitter.com/documenso"
target="_blank" target="_blank"
className="text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-foreground hover:text-foreground/80"
> >
<Twitter className="h-6 w-6" /> <FaXTwitter className="h-6 w-6" />
</Link> </Link>
<Link <Link
href="https://github.com/documenso/documenso" href="https://github.com/documenso/documenso"
target="_blank" target="_blank"
className="text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-foreground hover:text-foreground/80"
> >
<Github className="h-6 w-6" /> <LuGithub className="h-6 w-6" />
</Link> </Link>
<Link <Link
href="https://documen.so/discord" href="https://documen.so/discord"
target="_blank" target="_blank"
className="text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-foreground hover:text-foreground/80"
> >
<MessagesSquare className="h-6 w-6" /> <LiaDiscord className="h-7 w-7" />
</Link> </Link>
</div> </div>
</SheetContent> </SheetContent>

View File

@ -19,7 +19,7 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]" className="h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
/> />
</div> </div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24"> <h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
@ -30,41 +30,53 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8"> <div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2" degrees={45} gradient> <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]"> <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"> <p className="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
<strong className="block">Open Source or Hosted.</strong> <strong className="block">Open Source or Hosted.</strong>
Its up to you. Either clone our repository or rely on our easy to use hosting Its up to you. Either clone our repository or rely on our easy to use hosting
solution. solution.
</p> </p>
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6"> <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" /> <Image
src={cardOpenFigure}
alt="its fast"
className="max-w-[80%] dark:contrast-[70%] dark:hue-rotate-180 dark:invert lg:max-w-full"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Build on top.</strong> <strong className="block">Build on top.</strong>
Make it your own through advanced customization and adjustability. Make it your own through advanced customization and adjustability.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardBuildFigure} alt="its fast" className="w-full max-w-xs" /> <Image
src={cardBuildFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Template Store (Soon).</strong> <strong className="block">Template Store (Soon).</strong>
Choose a template from the community app store. Or submit your own template for others Choose a template from the community app store. Or submit your own template for others
to use. to use.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardTemplateFigure} alt="its fast" className="w-full max-w-sm" /> <Image
src={cardTemplateFigure}
alt="its fast"
className="w-full max-w-sm dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,9 +1,8 @@
'use client'; 'use client';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
export type PasswordRevealProps = { export type PasswordRevealProps = {
password: string; password: string;
}; };

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { HTMLAttributes, useMemo, useState } from 'react'; import { HTMLAttributes, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
@ -11,8 +11,6 @@ import { usePlausible } from 'next-plausible';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { ClaimPlanDialog } from './claim-plan-dialog';
export type PricingTableProps = HTMLAttributes<HTMLDivElement>; export type PricingTableProps = HTMLAttributes<HTMLDivElement>;
const SELECTED_PLAN_BAR_LAYOUT_ID = 'selected-plan-bar'; const SELECTED_PLAN_BAR_LAYOUT_ID = 'selected-plan-bar';
@ -27,24 +25,19 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
: 'MONTHLY', : 'MONTHLY',
); );
const planId = useMemo(() => {
if (period === 'MONTHLY') {
return process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
}
return process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID;
}, [period]);
return ( return (
<div className={cn('', className)} {...props}> <div className={cn('', className)} {...props}>
<div className="flex items-center justify-center gap-x-6"> <div className="flex items-center justify-center gap-x-6">
<AnimatePresence> <AnimatePresence>
<motion.button <motion.button
key="MONTHLY" key="MONTHLY"
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', { className={cn(
'text-slate-900': period === 'MONTHLY', 'text-muted-foreground relative flex items-center gap-x-2.5 px-1 py-2.5',
'hover:text-slate-900/80': period !== 'MONTHLY', {
})} 'text-foreground': period === 'MONTHLY',
'hover:text-foreground/80': period !== 'MONTHLY',
},
)}
onClick={() => setPeriod('MONTHLY')} onClick={() => setPeriod('MONTHLY')}
> >
Monthly Monthly
@ -58,14 +51,17 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<motion.button <motion.button
key="YEARLY" key="YEARLY"
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', { className={cn(
'text-slate-900': period === 'YEARLY', 'text-muted-foreground relative flex items-center gap-x-2.5 px-1 py-2.5',
'hover:text-slate-900/80': period !== 'YEARLY', {
})} 'text-foreground': period === 'YEARLY',
'hover:text-foreground/80': period !== 'YEARLY',
},
)}
onClick={() => setPeriod('YEARLY')} onClick={() => setPeriod('YEARLY')}
> >
Yearly Yearly
<div className="block rounded-full bg-slate-200 px-2 py-0.5 text-xs text-slate-700"> <div className="bg-muted text-foreground block rounded-full px-2 py-0.5 text-xs">
Save $60 Save $60
</div> </div>
{period === 'YEARLY' && ( {period === 'YEARLY' && (
@ -80,40 +76,40 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 md:grid-cols-2 lg:grid-cols-3"> <div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 md:grid-cols-2 lg:grid-cols-3">
<div <div
data-plan="self-hosted" data-plan="free"
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5" className="bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border px-8 py-12 shadow-lg"
> >
<p className="text-4xl font-medium text-slate-900">Self Hosted</p> <p className="text-foreground text-4xl font-medium">Free Plan</p>
<p className="text-primary mt-2.5 text-xl font-medium">Free</p> <p className="text-primary mt-2.5 text-xl font-medium">$0</p>
<p className="mt-4 max-w-[30ch] text-center text-slate-900"> <p className="text-foreground mt-4 max-w-[30ch] text-center">
For small teams and individuals who need a simple solution For small teams and individuals with basic needs.
</p> </p>
<Link <Button className="rounded-full text-base" asChild>
href="https://github.com/documenso/documenso" <Link
target="_blank" href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
className="mt-6" target="_blank"
onClick={() => event('view-github')} className="mt-6"
> >
<Button className="rounded-full text-base">View on Github</Button> Signup Now
</Link> </Link>
</Button>
<div className="mt-8 flex w-full flex-col divide-y"> <div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Host your own instance</p> <p className="text-foreground py-4">5 standard documents per month</p>
<p className="py-4 text-slate-900">Full Control</p> <p className="text-foreground py-4">Up to 10 recipients per document</p>
<p className="py-4 text-slate-900">Customizability</p> <p className="text-foreground py-4">No credit card required</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 className="flex-1" />
</div> </div>
<div <div
data-plan="community" 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" className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border-2 px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380]"
> >
<p className="text-4xl font-medium text-slate-900">Community</p> <p className="text-foreground text-4xl font-medium">Early Adopters</p>
<div className="text-primary mt-2.5 text-xl font-medium"> <div className="text-primary mt-2.5 text-xl font-medium">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>} {period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
@ -121,34 +117,44 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</AnimatePresence> </AnimatePresence>
</div> </div>
<p className="mt-4 max-w-[30ch] text-center text-slate-900"> <p className="text-foreground mt-4 max-w-[30ch] text-center">
For fast-growing companies that aim to scale across multiple teams. For fast-growing companies that aim to scale across multiple teams.
</p> </p>
<ClaimPlanDialog planId={planId}> <Button className="mt-6 rounded-full text-base" asChild>
<Button className="mt-6 rounded-full text-base">Signup Now</Button> <Link href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}>Signup Now</Link>
</ClaimPlanDialog> </Button>
<div className="mt-8 flex w-full flex-col divide-y"> <div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Documenso Early Adopter Deal:</p> <p className="text-foreground py-4 font-medium">
<p className="py-4 text-slate-900">Join the movement</p> {' '}
<p className="py-4 text-slate-900">Simple signing solution</p> <a href="https://documenso.com/blog/early-adopters" target="_blank">
<p className="py-4 text-slate-900">Email and Slack assistance</p> The Early Adopter Deal:
<p className="py-4 text-slate-900"> </a>
<strong>Includes all upcoming features</strong>
</p> </p>
<p className="py-4 text-slate-900">Fixed, straightforward pricing</p> <p className="text-foreground py-4">Join the movement</p>
<p className="text-foreground py-4">Simple signing solution</p>
<p className="text-foreground py-4">Email, Discord and Slack assistance</p>
<p className="text-foreground py-4">
<strong>
{' '}
<a href="https://documenso.com/blog/early-adopters" target="_blank">
Includes all upcoming features
</a>
</strong>
</p>
<p className="text-foreground py-4">Fixed, straightforward pricing</p>
</div> </div>
</div> </div>
<div <div
data-plan="enterprise" 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" className="bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border px-8 py-12 shadow-lg"
> >
<p className="text-4xl font-medium text-slate-900">Enterprise</p> <p className="text-foreground text-4xl font-medium">Enterprise</p>
<p className="text-primary mt-2.5 text-xl font-medium">Pricing on request</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"> <p className="text-foreground mt-4 max-w-[30ch] text-center">
For large organizations that need extra flexibility and control. For large organizations that need extra flexibility and control.
</p> </p>
@ -162,12 +168,12 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</Link> </Link>
<div className="mt-8 flex w-full flex-col divide-y"> <div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Everything in Community, plus:</p> <p className="text-foreground py-4 font-medium">Everything in Early Adopters, plus:</p>
<p className="py-4 text-slate-900">Custom Subdomain</p> <p className="text-foreground py-4">Custom Subdomain</p>
<p className="py-4 text-slate-900">Compliance Check</p> <p className="text-foreground py-4">Compliance Check</p>
<p className="py-4 text-slate-900">Guaranteed Uptime</p> <p className="text-foreground py-4">Guaranteed Uptime</p>
<p className="py-4 text-slate-900">Reporting & Analysis</p> <p className="text-foreground py-4">Reporting & Analysis</p>
<p className="py-4 text-slate-900">24/7 Support</p> <p className="text-foreground py-4">24/7 Support</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -23,7 +23,7 @@ export const ShareConnectPaidWidgetBento = ({
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]" className="h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
/> />
</div> </div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24"> <h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
@ -34,54 +34,70 @@ export const ShareConnectPaidWidgetBento = ({
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8"> <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> <Card className="col-span-2 lg:col-span-1" degrees={120} gradient>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Easy Sharing (Soon).</strong> <strong className="block">Easy Sharing (Soon).</strong>
Receive your personal link to share with everyone you care about. Receive your personal link to share with everyone you care about.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardSharingFigure} alt="its fast" className="w-full max-w-xs" /> <Image
src={cardSharingFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Connections (Soon).</strong> <strong className="block">Connections (Soon).</strong>
Create connections and automations with Zapier and more to integrate with your Create connections and automations with Zapier and more to integrate with your
favorite tools. favorite tools.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardConnectionsFigure} alt="its fast" className="w-full max-w-sm" /> <Image
src={cardConnectionsFigure}
alt="its fast"
className="w-full max-w-sm dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Get paid (Soon).</strong> <strong className="block">Get paid (Soon).</strong>
Integrated payments with stripe so you dont have to worry about getting paid. Integrated payments with stripe so you dont have to worry about getting paid.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardPaidFigure} alt="its fast" className="w-full max-w-[14rem]" /> <Image
src={cardPaidFigure}
alt="its fast"
className="w-full max-w-[14rem] dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">React Widget (Soon).</strong> <strong className="block">React Widget (Soon).</strong>
Easily embed Documenso into your product. Simply copy and paste our react widget into Easily embed Documenso into your product. Simply copy and paste our react widget into
your application. your application.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardWidgetFigure} alt="its fast" className="w-full max-w-xs" /> <Image
src={cardWidgetFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -0,0 +1,233 @@
'use server';
import { createElement } from 'react';
import { DateTime } from 'luxon';
import { PDFDocument } from 'pdf-lib';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed';
import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email';
import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf';
import { alphaid } from '@documenso/lib/universal/id';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { prisma } from '@documenso/prisma';
import {
DocumentDataType,
DocumentStatus,
FieldType,
Prisma,
ReadStatus,
SendStatus,
SigningStatus,
} from '@documenso/prisma/client';
const ZCreateSinglePlayerDocumentSchema = z.object({
documentData: z.object({
data: z.string(),
type: z.nativeEnum(DocumentDataType),
}),
documentName: z.string(),
signer: z.object({
email: z.string().email().min(1),
name: z.string(),
signature: z.string(),
}),
fields: z.array(
z.object({
page: z.number(),
type: z.nativeEnum(FieldType),
positionX: z.number(),
positionY: z.number(),
width: z.number(),
height: z.number(),
}),
),
});
export type TCreateSinglePlayerDocumentSchema = z.infer<typeof ZCreateSinglePlayerDocumentSchema>;
/**
* Create and self signs a document.
*
* Returns the document token.
*/
export const createSinglePlayerDocument = async (
value: TCreateSinglePlayerDocumentSchema,
): Promise<string> => {
const { signer, fields, documentData, documentName } =
ZCreateSinglePlayerDocumentSchema.parse(value);
const document = await getFile({
data: documentData.data,
type: documentData.type,
});
const doc = await PDFDocument.load(document);
const createdAt = new Date();
const isBase64 = signer.signature.startsWith('data:image/png;base64,');
const signatureImageAsBase64 = isBase64 ? signer.signature : null;
const typedSignature = !isBase64 ? signer.signature : null;
// Update the document with the fields inserted.
for (const field of fields) {
const isSignatureField = field.type === FieldType.SIGNATURE;
await insertFieldInPDF(doc, {
...mapField(field, signer),
Signature: isSignatureField
? {
created: createdAt,
signatureImageAsBase64,
typedSignature,
// Dummy data.
id: -1,
recipientId: -1,
fieldId: -1,
}
: null,
// Dummy data.
id: -1,
documentId: -1,
recipientId: -1,
});
}
const pdfBytes = await doc.save();
const documentToken = await prisma.$transaction(
async (tx) => {
const documentToken = alphaid();
// Fetch service user who will be the owner of the document.
const serviceUser = await tx.user.findFirstOrThrow({
where: {
email: SERVICE_USER_EMAIL,
},
});
const documentDataBytes = Buffer.from(pdfBytes).toString('base64');
const { id: documentDataId } = await tx.documentData.create({
data: {
type: DocumentDataType.BYTES_64,
data: documentDataBytes,
initialData: documentDataBytes,
},
});
// Create document.
const document = await tx.document.create({
data: {
title: documentName,
status: DocumentStatus.COMPLETED,
documentDataId,
userId: serviceUser.id,
createdAt,
},
});
// Create recipient.
const recipient = await tx.recipient.create({
data: {
documentId: document.id,
name: signer.name,
email: signer.email,
token: documentToken,
signedAt: createdAt,
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
});
// Create fields and signatures.
await Promise.all(
fields.map(async (field) => {
const insertedField = await tx.field.create({
data: {
documentId: document.id,
recipientId: recipient.id,
...mapField(field, signer),
},
});
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
await tx.signature.create({
data: {
fieldId: insertedField.id,
signatureImageAsBase64,
typedSignature,
recipientId: recipient.id,
},
});
}
}),
);
return documentToken;
},
{
maxWait: 5000,
timeout: 30000,
},
);
const template = createElement(DocumentSelfSignedEmailTemplate, {
documentName: documentName,
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
});
// Send email to signer.
await mailer.sendMail({
to: {
address: signer.email,
name: signer.name,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'Document signed',
html: render(template),
text: render(template, { plainText: true }),
attachments: [{ content: Buffer.from(pdfBytes), filename: documentName }],
});
return documentToken;
};
/**
* Map the fields provided by the user to fields compatible with Prisma.
*
* Signature fields are handled separately.
*
* @param field The field passed in by the user.
* @param signer The details of the person who is signing this document.
* @returns A field compatible with Prisma.
*/
const mapField = (
field: TCreateSinglePlayerDocumentSchema['fields'][number],
signer: TCreateSinglePlayerDocumentSchema['signer'],
) => {
const customText = match(field.type)
.with(FieldType.DATE, () => DateTime.now().toFormat('yyyy-MM-dd hh:mm a'))
.with(FieldType.EMAIL, () => signer.email)
.with(FieldType.NAME, () => signer.name)
.otherwise(() => '');
return {
type: field.type,
page: field.page,
positionX: new Prisma.Decimal(field.positionX),
positionY: new Prisma.Decimal(field.positionY),
width: new Prisma.Decimal(field.width),
height: new Prisma.Decimal(field.height),
customText,
inserted: true,
};
};

View File

@ -0,0 +1,103 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { DocumentStatus, Signature } from '@documenso/prisma/client';
import { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import signingCelebration from '~/assets/signing-celebration.png';
import { ConfettiScreen } from '~/components/(marketing)/confetti-screen';
interface SinglePlayerModeSuccessProps {
className?: string;
document: DocumentWithRecipient;
signatures: Signature[];
}
export const SinglePlayerModeSuccess = ({
className,
document,
signatures,
}: SinglePlayerModeSuccessProps) => {
const { getFlag } = useFeatureFlags();
const isConfettiEnabled = getFlag('marketing_spm_confetti');
const [showDocumentDialog, setShowDocumentDialog] = useState(false);
const { documentData } = document;
useEffect(() => {
window.scrollTo({ top: 0 });
}, []);
return (
<div className="flex min-h-[calc(100vh-10rem)] flex-col items-center justify-center sm:min-h-[calc(100vh-13rem)]">
{isConfettiEnabled && (
<ConfettiScreen duration={3000} gravity={0.075} initialVelocityY={50} wind={0.005} />
)}
<h2 className="relative z-10 text-center text-2xl font-semibold leading-normal md:text-3xl lg:mb-2 lg:text-4xl">
You have signed
<span className="mt-2 block">{document.title}</span>
</h2>
<SigningCard3D
className="mt-8"
name={document.Recipient.name || document.Recipient.email}
signature={signatures.at(0)}
signingCelebrationImage={signingCelebration}
/>
<div className="relative mt-8 w-full">
<div className={cn('flex flex-col items-center', className)}>
<div className="grid w-full max-w-sm grid-cols-2 gap-4">
<DocumentShareButton
documentId={document.id}
token={document.Recipient.token}
className="flex-1 bg-transparent backdrop-blur-sm"
/>
<DocumentDownloadButton
className="flex-1 bg-transparent backdrop-blur-sm"
fileName={document.title}
documentData={document.documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
<Button onClick={() => setShowDocumentDialog(true)} className="z-10 col-span-2">
Show document
</Button>
</div>
</div>
</div>
<p className="text-muted-foreground/60 mt-16 text-center text-sm">
Create a{' '}
<Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
target="_blank"
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
>
free account
</Link>{' '}
to access your signed documents at any time
</p>
<DocumentDialog
documentData={documentData}
open={showDocumentDialog}
onOpenChange={setShowDocumentDialog}
/>
</div>
);
};

View File

@ -31,7 +31,7 @@ import { FormErrorMessage } from '../form/form-error-message';
const ZWidgetFormSchema = z const ZWidgetFormSchema = z
.object({ .object({
email: z.string().email({ message: 'Please enter a valid email address.' }), email: z.string().email({ message: 'Please enter a valid email address.' }),
name: z.string().min(3, { message: 'Please enter a valid name.' }), name: z.string().trim().min(3, { message: 'Please enter a valid name.' }),
}) })
.and( .and(
z.union([ z.union([
@ -41,7 +41,7 @@ const ZWidgetFormSchema = z
}), }),
z.object({ z.object({
signatureDataUrl: z.null().or(z.string().max(0)), signatureDataUrl: z.null().or(z.string().max(0)),
signatureText: z.string().min(1), signatureText: z.string().trim().min(1),
}), }),
]), ]),
); );
@ -181,16 +181,16 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
{...props} {...props}
> >
<div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8"> <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"> <div className="text-muted-foreground col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed lg:col-span-7">
{children} {children}
</div> </div>
<form <form
className="col-span-12 flex flex-col rounded-2xl bg-[#F7F7F7] p-6 lg:col-span-5" className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
<h3 className="text-2xl font-semibold">Sign up for the community plan</h3> <h3 className="text-2xl font-semibold">Sign up for the early adopters plan</h3>
<p className="mt-2 text-xs text-[#AFAFAF]"> <p className="text-muted-foreground mt-2 text-xs">
with Timur Ercan & Lucas Smith from Documenso with Timur Ercan & Lucas Smith from Documenso
</p> </p>
@ -198,7 +198,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<AnimatePresence> <AnimatePresence>
<motion.div key="email"> <motion.div key="email">
<label htmlFor="email" className="text-lg font-semibold text-slate-900 lg:text-xl"> <label htmlFor="email" className="text-foreground text-lg font-semibold lg:text-xl">
Whats your email? Whats your email?
</label> </label>
@ -211,7 +211,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
id="email" id="email"
type="email" type="email"
placeholder="" placeholder=""
className="w-full bg-white pr-16" className="bg-background w-full pr-16"
disabled={isSubmitting} disabled={isSubmitting}
onKeyDown={(e) => onKeyDown={(e) =>
field.value !== '' && field.value !== '' &&
@ -255,7 +255,10 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
transform: 'translateX(25%)', transform: 'translateX(25%)',
}} }}
> >
<label htmlFor="name" className="text-lg font-semibold text-slate-900 lg:text-xl"> <label
htmlFor="name"
className="text-foreground text-lg font-semibold lg:text-xl"
>
and your name? and your name?
</label> </label>
@ -268,7 +271,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
id="name" id="name"
type="text" type="text"
placeholder="" placeholder=""
className="w-full bg-white pr-16" className="bg-background w-full pr-16"
disabled={isSubmitting} disabled={isSubmitting}
onKeyDown={(e) => onKeyDown={(e) =>
field.value !== '' && field.value !== '' &&
@ -300,11 +303,11 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<div className="mt-12 flex-1" /> <div className="mt-12 flex-1" />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs text-[#AFAFAF]">{stepsRemaining} step(s) until signed</p> <p className="text-muted-foreground text-xs">{stepsRemaining} step(s) until signed</p>
<p className="block text-xs text-[#AFAFAF] md:hidden">Minimise contract</p> <p className="text-muted-foreground block text-xs md:hidden">Minimise contract</p>
</div> </div>
<div className="relative mt-2.5 h-[2px] w-full bg-[#E9E9E9]"> <div className="bg-background relative mt-2.5 h-[2px] w-full">
<div <div
className={cn('bg-primary/60 absolute inset-y-0 left-0 duration-200', { className={cn('bg-primary/60 absolute inset-y-0 left-0 duration-200', {
'w-1/3': stepsRemaining === 3, 'w-1/3': stepsRemaining === 3,
@ -322,13 +325,17 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
> >
<div className="flex h-28 items-center justify-center pb-6"> <div className="flex h-28 items-center justify-center pb-6">
{!signatureText && signatureDataUrl && ( {!signatureText && signatureDataUrl && (
<img src={signatureDataUrl} alt="user signature" className="h-full" /> <img
src={signatureDataUrl}
alt="user signature"
className="h-full dark:invert"
/>
)} )}
{signatureText && ( {signatureText && (
<p <p
className={cn( className={cn(
'text-4xl font-semibold text-slate-900 [font-family:var(--font-caveat)]', 'text-foreground text-4xl font-semibold [font-family:var(--font-caveat)]',
)} )}
> >
{signatureText} {signatureText}
@ -342,7 +349,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
> >
<Input <Input
id="signatureText" id="signatureText"
className="border-none p-0 text-sm text-slate-700 placeholder:text-[#D6D6D6] focus-visible:ring-0" className="text-foreground placeholder:text-muted-foreground border-none p-0 text-sm focus-visible:ring-0"
placeholder="Draw or type name here" placeholder="Draw or type name here"
disabled={isSubmitting} disabled={isSubmitting}
{...register('signatureText', { {...register('signatureText', {
@ -356,7 +363,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<Button <Button
type="submit" type="submit"
className="h-8 disabled:bg-[#ECEEED] disabled:text-[#C6C6C6] disabled:hover:bg-[#ECEEED]" className="disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted h-8"
disabled={!isValid || isSubmitting} disabled={!isValid || isSubmitting}
> >
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />} {isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
@ -370,7 +377,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
</Card> </Card>
<Dialog open={showSigningDialog} onOpenChange={setShowSigningDialog}> <Dialog open={showSigningDialog} onOpenChange={setShowSigningDialog}>
<DialogContent> <DialogContent position="center">
<DialogHeader> <DialogHeader>
<DialogTitle>Add your signature</DialogTitle> <DialogTitle>Add your signature</DialogTitle>
</DialogHeader> </DialogHeader>
@ -384,6 +391,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<SignaturePad <SignaturePad
className="aspect-video w-full rounded-md border" className="aspect-video w-full rounded-md border"
defaultValue={signatureDataUrl || ''}
onChange={setDraftSignatureDataUrl} onChange={setDraftSignatureDataUrl}
/> />

View File

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

View File

@ -2,7 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { hashSync } from '@documenso/lib/server-only/auth/hash'; import { TEarlyAdopterCheckoutMetadataSchema } from '@documenso/ee/server-only/stripe/webhook/early-adopter-checkout-metadata';
import { redis } from '@documenso/lib/server-only/redis'; import { redis } from '@documenso/lib/server-only/redis';
import { stripe } from '@documenso/lib/server-only/stripe'; import { stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -36,64 +36,38 @@ export default async function handler(
where: { where: {
email: email.toLowerCase(), email: email.toLowerCase(),
}, },
include: {
Subscription: true,
},
}); });
if (user && user.Subscription.length > 0) { if (user) {
return res.status(200).json({ return res.status(200).json({
redirectUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/login`, redirectUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/signin`,
}); });
} }
const password = Math.random().toString(36).slice(2, 9); const clientReferenceId = randomUUID();
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) { if (signatureDataUrl) {
await redis.set(`signature:${signatureDataUrlKey}`, signatureDataUrl, { await redis.set(`signature:${clientReferenceId}`, signatureDataUrl, {
// expire in 7 days // expire in 7 days
ex: 60 * 60 * 24 * 7, ex: 60 * 60 * 24 * 7,
}); });
} }
const metadata: Record<string, string> = { const metadata: TEarlyAdopterCheckoutMetadataSchema = {
name, name,
email, email,
signatureText: signatureText || name, signatureText: signatureText || name,
source: 'landing', source: 'marketing',
}; };
if (signatureDataUrl) { if (signatureDataUrl) {
metadata.signatureDataUrl = signatureDataUrlKey; metadata.signatureDataUrl = clientReferenceId;
} }
const checkout = await stripe.checkout.sessions.create({ const checkout = await stripe.checkout.sessions.create({
customer_email: email, customer_email: email,
client_reference_id: userId.toString(), // Using the UUID here means our webhook will not try to use it as a user ID.
payment_method_types: ['card'], client_reference_id: clientReferenceId,
line_items: [ line_items: [
{ {
price: planId, price: planId,
@ -104,9 +78,7 @@ export default async function handler(
metadata, metadata,
allow_promotion_codes: true, allow_promotion_codes: true,
success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`, success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/pricing?email=${encodeURIComponent( cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}`,
email,
)}&name=${encodeURIComponent(name)}&planId=${planId}&cancelled=true`,
}); });
if (!checkout.url) { if (!checkout.url) {

View File

@ -0,0 +1,7 @@
import handlerFeatureFlagAll from '@documenso/lib/server-only/feature-flags/all';
export const config = {
runtime: 'edge',
};
export default handlerFeatureFlagAll;

View File

@ -0,0 +1,7 @@
import handlerFeatureFlagGet from '@documenso/lib/server-only/feature-flags/get';
export const config = {
runtime: 'edge',
};
export default handlerFeatureFlagGet;

View File

@ -0,0 +1,8 @@
import * as trpcNext from '@documenso/trpc/server/adapters/next';
import { createTrpcContext } from '@documenso/trpc/server/context';
import { appRouter } from '@documenso/trpc/server/router';
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: async ({ req, res }) => createTrpcContext({ req, res }),
});

View File

@ -0,0 +1,10 @@
'use client';
import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { ThemeProviderProps } from 'next-themes/dist/types';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@ -0,0 +1,39 @@
'use client';
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import posthog from 'posthog-js';
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
export function PostHogPageview() {
const postHogConfig = extractPostHogConfig();
const pathname = usePathname();
const searchParams = useSearchParams();
if (typeof window !== 'undefined' && postHogConfig) {
posthog.init(postHogConfig.key, {
api_host: postHogConfig.host,
disable_session_recording: true,
});
}
useEffect(() => {
if (!postHogConfig || !pathname) {
return;
}
let url = window.origin + pathname;
if (searchParams && searchParams.toString()) {
url = url + `?${searchParams.toString()}`;
}
posthog.capture('$pageview', {
$current_url: url,
});
}, [pathname, searchParams, postHogConfig]);
return null;
}

BIN
apps/web/example/cert.p12 Normal file

Binary file not shown.

View File

@ -2,14 +2,17 @@
const path = require('path'); const path = require('path');
const { version } = require('./package.json'); const { version } = require('./package.json');
const { parsed: env } = require('dotenv').config({ const ENV_FILES = ['.env', '.env.local', `.env.${process.env.NODE_ENV || 'development'}`];
path: path.join(__dirname, '../../.env.local'),
ENV_FILES.forEach((file) => {
require('dotenv').config({
path: path.join(__dirname, `../../${file}`),
});
}); });
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const config = { const config = {
experimental: { experimental: {
serverActions: true,
serverActionsBodySizeLimit: '50mb', serverActionsBodySizeLimit: '50mb',
}, },
reactStrictMode: true, reactStrictMode: true,
@ -22,12 +25,55 @@ const config = {
], ],
env: { env: {
APP_VERSION: version, APP_VERSION: version,
NEXT_PUBLIC_PROJECT: 'web',
}, },
modularizeImports: { modularizeImports: {
'lucide-react': { 'lucide-react': {
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
}, },
}, },
webpack: (config, { isServer }) => {
// fixes: Module not found: Cant resolve ../build/Release/canvas.node
if (isServer) {
config.resolve.alias.canvas = false;
}
return config;
},
async rewrites() {
return [
{
source: '/ingest/:path*',
destination: 'https://eu.posthog.com/:path*',
},
];
},
async redirects() {
return [
{
permanent: true,
source: '/documents/:id/sign',
destination: '/sign/:token',
has: [
{
type: 'query',
key: 'token',
},
],
},
{
permanent: true,
source: '/documents/:id/signed',
destination: '/sign/:token',
has: [
{
type: 'query',
key: 'token',
},
],
},
];
},
}; };
module.exports = config; module.exports = config;

View File

@ -25,8 +25,8 @@
"lucide-react": "^0.279.0", "lucide-react": "^0.279.0",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"micro": "^10.0.1", "micro": "^10.0.1",
"next": "13.4.19", "next": "14.0.0",
"next-auth": "4.22.3", "next-auth": "4.24.3",
"next-plausible": "^3.10.1", "next-plausible": "^3.10.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"perfect-freehand": "^1.2.0", "perfect-freehand": "^1.2.0",
@ -36,12 +36,12 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.43.9",
"react-icons": "^4.8.0", "react-icons": "^4.11.0",
"react-rnd": "^10.4.1", "react-rnd": "^10.4.1",
"sharp": "0.32.5", "sharp": "0.32.5",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"typescript": "5.1.6", "typescript": "5.2.2",
"zod": "^3.21.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@types/formidable": "^2.0.6", "@types/formidable": "^2.0.6",

View File

@ -7,6 +7,7 @@ declare namespace NodeJS {
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string; NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string; NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
NEXT_PRIVATE_STRIPE_API_KEY: string; NEXT_PRIVATE_STRIPE_API_KEY: string;
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string; NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

View File

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

View File

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

View File

@ -0,0 +1,125 @@
'use client';
import { useTransition } from 'react';
import Link from 'next/link';
import { Loader } from 'lucide-react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { FindResultSet } from '@documenso/lib/types/find-result-set';
import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
import { Document, User } from '@documenso/prisma/client';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
export type DocumentsDataTableProps = {
results: FindResultSet<
Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
}
>;
};
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
const onPaginationChange = (page: number, perPage: number) => {
startTransition(() => {
updateSearchParams({
page,
perPage,
});
});
};
return (
<div className="relative">
<DataTable
columns={[
{
header: 'Created',
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
header: 'Title',
accessorKey: 'title',
cell: ({ row }) => {
return (
<div className="block max-w-[5rem] truncate font-medium md:max-w-[10rem]">
{row.original.title}
</div>
);
},
},
{
header: 'Owner',
accessorKey: 'owner',
cell: ({ row }) => {
const avatarFallbackText = row.original.User.name
? recipientInitials(row.original.User.name)
: row.original.User.email.slice(0, 1).toUpperCase();
return (
<Tooltip delayDuration={200}>
<TooltipTrigger>
<Link href={`/admin/users/${row.original.User.id}`}>
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
<AvatarFallback className="text-xs text-gray-400">
{avatarFallbackText}
</AvatarFallback>
</Avatar>
</Link>
</TooltipTrigger>
<TooltipContent className="flex max-w-xs items-center gap-2">
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
<AvatarFallback className="text-xs text-gray-400">
{avatarFallbackText}
</AvatarFallback>
</Avatar>
<div className="text-muted-foreground flex flex-col text-sm">
<span>{row.original.User.name}</span>
<span>{row.original.User.email}</span>
</div>
</TooltipContent>
</Tooltip>
);
},
},
{
header: 'Last updated',
accessorKey: 'updatedAt',
cell: ({ row }) => <LocaleDate date={row.original.updatedAt} />,
},
{
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
},
]}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
{isPending && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
</div>
)}
</div>
);
};

View File

@ -0,0 +1,29 @@
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
import { DocumentsDataTable } from './data-table';
export type DocumentsPageProps = {
searchParams?: {
page?: string;
perPage?: string;
};
};
export default async function Documents({ searchParams = {} }: DocumentsPageProps) {
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 20;
const results = await findDocuments({
page,
perPage,
});
return (
<div>
<h2 className="text-4xl font-semibold">Manage documents</h2>
<div className="mt-8">
<DocumentsDataTable results={results} />
</div>
</div>
);
}

View File

@ -12,7 +12,7 @@ export type AdminSectionLayoutProps = {
}; };
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) { export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
if (!isAdmin(user)) { if (!isAdmin(user)) {
redirect('/documents'); redirect('/documents');
@ -20,7 +20,7 @@ export default async function AdminSectionLayout({ children }: AdminSectionLayou
return ( return (
<div className="mx-auto mt-16 w-full max-w-screen-xl px-4 md:px-8"> <div className="mx-auto mt-16 w-full max-w-screen-xl px-4 md:px-8">
<div className="grid grid-cols-12 gap-x-8 md:mt-8"> <div className="grid grid-cols-12 md:mt-8 md:gap-8">
<AdminNav className="col-span-12 md:col-span-3 md:flex" /> <AdminNav className="col-span-12 md:col-span-3 md:flex" />
<div className="col-span-12 mt-12 md:col-span-9 md:mt-0">{children}</div> <div className="col-span-12 mt-12 md:col-span-9 md:mt-0">{children}</div>

View File

@ -5,7 +5,7 @@ import { HTMLAttributes } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { BarChart3, User2 } from 'lucide-react'; import { BarChart3, FileStack, User2, Wallet2 } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -16,7 +16,13 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<div className={cn('flex gap-x-2.5 gap-y-2 md:flex-col', className)} {...props}> <div
className={cn(
'flex gap-x-2.5 gap-y-2 overflow-hidden overflow-x-auto md:flex-col',
className,
)}
{...props}
>
<Button <Button
variant="ghost" variant="ghost"
className={cn( className={cn(
@ -37,10 +43,40 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => {
'justify-start md:w-full', 'justify-start md:w-full',
pathname?.startsWith('/admin/users') && 'bg-secondary', pathname?.startsWith('/admin/users') && 'bg-secondary',
)} )}
disabled asChild
> >
<User2 className="mr-2 h-5 w-5" /> <Link href="/admin/users">
Users (Coming Soon) <User2 className="mr-2 h-5 w-5" />
Users
</Link>
</Button>
<Button
variant="ghost"
className={cn(
'justify-start md:w-full',
pathname?.startsWith('/admin/documents') && 'bg-secondary',
)}
asChild
>
<Link href="/admin/documents">
<FileStack className="mr-2 h-5 w-5" />
Documents
</Link>
</Button>
<Button
variant="ghost"
className={cn(
'justify-start md:w-full',
pathname?.startsWith('/admin/subscriptions') && 'bg-secondary',
)}
asChild
>
<Link href="/admin/subscriptions">
<Wallet2 className="mr-2 h-5 w-5" />
Subscriptions
</Link>
</Button> </Button>
</div> </div>
); );

View File

@ -32,7 +32,7 @@ export default async function AdminStatsPage() {
<div> <div>
<h2 className="text-4xl font-semibold">Instance Stats</h2> <h2 className="text-4xl font-semibold">Instance Stats</h2>
<div className="mt-8 grid flex-1 grid-cols-1 gap-4 md:grid-cols-4"> <div className="mt-8 grid flex-1 grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<CardMetric icon={UserIcon} title="Total Users" value={usersCount} /> <CardMetric icon={UserIcon} title="Total Users" value={usersCount} />
<CardMetric icon={File} title="Total Documents" value={docStats.ALL} /> <CardMetric icon={File} title="Total Documents" value={docStats.ALL} />
<CardMetric <CardMetric
@ -43,7 +43,7 @@ export default async function AdminStatsPage() {
<CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} /> <CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} />
</div> </div>
<div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-2"> <div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-1 lg:grid-cols-2">
<div> <div>
<h3 className="text-3xl font-semibold">Document metrics</h3> <h3 className="text-3xl font-semibold">Document metrics</h3>

View File

@ -0,0 +1,65 @@
import Link from 'next/link';
import { findSubscriptions } from '@documenso/lib/server-only/admin/get-all-subscriptions';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@documenso/ui/primitives/table';
export default async function Subscriptions() {
const subscriptions = await findSubscriptions();
return (
<div>
<h2 className="text-4xl font-semibold">Manage subscriptions</h2>
<div className="mt-8">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created At</TableHead>
<TableHead>Ends On</TableHead>
<TableHead>User ID</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{subscriptions.map((subscription, index) => (
<TableRow key={index}>
<TableCell>{subscription.id}</TableCell>
<TableCell>{subscription.status}</TableCell>
<TableCell>
{subscription.createdAt
? new Date(subscription.createdAt).toLocaleDateString(undefined, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})
: 'N/A'}
</TableCell>
<TableCell>
{subscription.periodEnd
? new Date(subscription.periodEnd).toLocaleDateString(undefined, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})
: 'N/A'}
</TableCell>
<TableCell>
<Link href={`/admin/users/${subscription.userId}`}>{subscription.userId}</Link>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@ -0,0 +1,141 @@
'use client';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import { Combobox } from '@documenso/ui/primitives/combobox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
const ZUserFormSchema = ZUpdateProfileMutationByAdminSchema.omit({ id: true });
type TUserFormSchema = z.infer<typeof ZUserFormSchema>;
export default function UserPage({ params }: { params: { id: number } }) {
const { toast } = useToast();
const router = useRouter();
const { data: user } = trpc.profile.getUser.useQuery(
{
id: Number(params.id),
},
{
enabled: !!params.id,
},
);
const roles = user?.roles ?? [];
const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation();
const form = useForm<TUserFormSchema>({
resolver: zodResolver(ZUserFormSchema),
values: {
name: user?.name ?? '',
email: user?.email ?? '',
roles: user?.roles ?? [],
},
});
const onSubmit = async ({ name, email, roles }: TUserFormSchema) => {
try {
await updateUserMutation({
id: Number(user?.id),
name,
email,
roles,
});
router.refresh();
toast({
title: 'Profile updated',
description: 'Your profile has been updated.',
duration: 5000,
});
} catch (e) {
toast({
title: 'Error',
description: 'An error occurred while updating your profile.',
variant: 'destructive',
});
}
};
return (
<div>
<h2 className="text-4xl font-semibold">Manage {user?.name}'s profile</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset className="mt-6 flex w-full flex-col gap-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="text-muted-foreground">Name</FormLabel>
<FormControl>
<Input type="text" {...field} value={field.value ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="text-muted-foreground">Email</FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="roles"
render={({ field: { onChange } }) => (
<FormItem>
<fieldset className="flex flex-col gap-2">
<FormLabel className="text-muted-foreground">Roles</FormLabel>
<FormControl>
<Combobox
listValues={roles}
onChange={(values: string[]) => onChange(values)}
/>
</FormControl>
<FormMessage />
</fieldset>
</FormItem>
)}
/>
<div className="mt-4">
<Button type="submit" loading={form.formState.isSubmitting}>
Update user
</Button>
</div>
</fieldset>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,143 @@
'use client';
import { useEffect, useState, useTransition } from 'react';
import Link from 'next/link';
import { Edit, Loader } from 'lucide-react';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { Document, Role, Subscription } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Input } from '@documenso/ui/primitives/input';
type UserData = {
id: number;
name: string | null;
email: string;
roles: Role[];
Subscription?: SubscriptionLite | null;
Document: DocumentLite[];
};
type SubscriptionLite = Pick<
Subscription,
'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd'
>;
type DocumentLite = Pick<Document, 'id'>;
type UsersDataTableProps = {
users: UserData[];
totalPages: number;
perPage: number;
page: number;
};
export const UsersDataTable = ({ users, totalPages, perPage, page }: UsersDataTableProps) => {
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
const [searchString, setSearchString] = useState('');
const debouncedSearchString = useDebouncedValue(searchString, 1000);
useEffect(() => {
startTransition(() => {
updateSearchParams({
search: debouncedSearchString,
page: 1,
perPage,
});
});
}, [debouncedSearchString]);
const onPaginationChange = (page: number, perPage: number) => {
startTransition(() => {
updateSearchParams({
page,
perPage,
});
});
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchString(e.target.value);
};
return (
<div className="relative">
<Input
className="my-6 flex flex-row gap-4"
type="text"
placeholder="Search by name or email"
value={searchString}
onChange={handleChange}
/>
<DataTable
columns={[
{
header: 'ID',
accessorKey: 'id',
cell: ({ row }) => <div>{row.original.id}</div>,
},
{
header: 'Name',
accessorKey: 'name',
cell: ({ row }) => <div>{row.original.name}</div>,
},
{
header: 'Email',
accessorKey: 'email',
cell: ({ row }) => <div>{row.original.email}</div>,
},
{
header: 'Roles',
accessorKey: 'roles',
cell: ({ row }) => row.original.roles.join(', '),
},
{
header: 'Subscription',
accessorKey: 'subscription',
cell: ({ row }) => row.original.Subscription?.status ?? 'NONE',
},
{
header: 'Documents',
accessorKey: 'documents',
cell: ({ row }) => {
return <div>{row.original.Document.length}</div>;
},
},
{
header: '',
accessorKey: 'edit',
cell: ({ row }) => {
return (
<Button className="w-24" asChild>
<Link href={`/admin/users/${row.original.id}`}>
<Edit className="-ml-1 mr-2 h-4 w-4" />
Edit
</Link>
</Button>
);
},
},
]}
data={users}
perPage={perPage}
currentPage={page}
totalPages={totalPages}
onPaginationChange={onPaginationChange}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
{isPending && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
</div>
)}
</div>
);
};

View File

@ -0,0 +1,9 @@
'use server';
import { findUsers } from '@documenso/lib/server-only/user/get-all-users';
export async function search(search: string, page: number, perPage: number) {
const results = await findUsers({ username: search, email: search, page, perPage });
return results;
}

View File

@ -0,0 +1,25 @@
import { UsersDataTable } from './data-table-users';
import { search } from './fetch-users.actions';
type AdminManageUsersProps = {
searchParams?: {
search?: string;
page?: number;
perPage?: number;
};
};
export default async function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) {
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 10;
const searchString = searchParams.search || '';
const { users, totalPages } = await search(searchString, page, perPage);
return (
<div>
<h2 className="text-4xl font-semibold">Manage users</h2>
<UsersDataTable users={users} totalPages={totalPages} page={page} perPage={perPage} />
</div>
);
}

View File

@ -4,7 +4,7 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Field, Recipient, User } from '@documenso/prisma/client'; import { DocumentData, Field, Recipient, User } from '@documenso/prisma/client';
import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -32,7 +32,7 @@ export type EditDocumentFormProps = {
document: DocumentWithData; document: DocumentWithData;
recipients: Recipient[]; recipients: Recipient[];
fields: Field[]; fields: Field[];
dataUrl: string; documentData: DocumentData;
}; };
type EditDocumentStep = 'signers' | 'fields' | 'subject'; type EditDocumentStep = 'signers' | 'fields' | 'subject';
@ -43,7 +43,7 @@ export const EditDocumentForm = ({
recipients, recipients,
fields, fields,
user: _user, user: _user,
dataUrl, documentData,
}: EditDocumentFormProps) => { }: EditDocumentFormProps) => {
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const router = useRouter();
@ -55,21 +55,18 @@ export const EditDocumentForm = ({
title: 'Add Signers', title: 'Add Signers',
description: 'Add the people who will sign the document.', description: 'Add the people who will sign the document.',
stepIndex: 1, stepIndex: 1,
onSubmit: () => onAddSignersFormSubmit,
}, },
fields: { fields: {
title: 'Add Fields', title: 'Add Fields',
description: 'Add all relevant fields for each recipient.', description: 'Add all relevant fields for each recipient.',
stepIndex: 2, stepIndex: 2,
onBackStep: () => setStep('signers'), onBackStep: () => setStep('signers'),
onSubmit: () => onAddFieldsFormSubmit,
}, },
subject: { subject: {
title: 'Add Subject', title: 'Add Subject',
description: 'Add the subject and message you wish to send to signers.', description: 'Add the subject and message you wish to send to signers.',
stepIndex: 3, stepIndex: 3,
onBackStep: () => setStep('fields'), onBackStep: () => setStep('fields'),
onSubmit: () => onAddSubjectFormSubmit,
}, },
}; };
@ -156,7 +153,7 @@ export const EditDocumentForm = ({
gradient gradient
> >
<CardContent className="p-2"> <CardContent className="p-2">
<LazyPDFViewer document={dataUrl} /> <LazyPDFViewer key={documentData.id} documentData={documentData} />
</CardContent> </CardContent>
</Card> </Card>
@ -169,6 +166,7 @@ export const EditDocumentForm = ({
{step === 'signers' && ( {step === 'signers' && (
<AddSignersFormPartial <AddSignersFormPartial
key={recipients.length}
documentFlow={documentFlow.signers} documentFlow={documentFlow.signers}
recipients={recipients} recipients={recipients}
fields={fields} fields={fields}
@ -179,6 +177,7 @@ export const EditDocumentForm = ({
{step === 'fields' && ( {step === 'fields' && (
<AddFieldsFormPartial <AddFieldsFormPartial
key={fields.length}
documentFlow={documentFlow.fields} documentFlow={documentFlow.fields}
recipients={recipients} recipients={recipients}
fields={fields} fields={fields}

View File

@ -10,7 +10,7 @@ export default function Loading() {
Documents Documents
</Link> </Link>
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl"> <h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
Loading Document... Loading Document...
</h1> </h1>
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8"> <div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">

View File

@ -7,7 +7,6 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document'; import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client'; import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
@ -30,11 +29,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
redirect('/documents'); redirect('/documents');
} }
const session = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
const document = await getDocumentById({ const document = await getDocumentById({
id: documentId, id: documentId,
userId: session.id, userId: user.id,
}).catch(() => null); }).catch(() => null);
if (!document || !document.documentData) { if (!document || !document.documentData) {
@ -43,18 +42,14 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
const { documentData } = document; const { documentData } = document;
const documentDataUrl = await getFile(documentData)
.then((buffer) => Buffer.from(buffer).toString('base64'))
.then((data) => `data:application/pdf;base64,${data}`);
const [recipients, fields] = await Promise.all([ const [recipients, fields] = await Promise.all([
await getRecipientsForDocument({ await getRecipientsForDocument({
documentId, documentId,
userId: session.id, userId: user.id,
}), }),
await getFieldsForDocument({ await getFieldsForDocument({
documentId, documentId,
userId: session.id, userId: user.id,
}), }),
]); ]);
@ -65,10 +60,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
Documents Documents
</Link> </Link>
<h1 <h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
className="mt-4 max-w-xs truncate text-2xl font-semibold md:text-3xl"
title={document.title}
>
{document.title} {document.title}
</h1> </h1>
@ -90,16 +82,16 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
<EditDocumentForm <EditDocumentForm
className="mt-8" className="mt-8"
document={document} document={document}
user={session} user={user}
recipients={recipients} recipients={recipients}
fields={fields} fields={fields}
dataUrl={documentDataUrl} documentData={documentData}
/> />
)} )}
{document.status === InternalDocumentStatus.COMPLETED && ( {document.status === InternalDocumentStatus.COMPLETED && (
<div className="mx-auto mt-12 max-w-2xl"> <div className="mx-auto mt-12 max-w-2xl">
<LazyPDFViewer document={documentDataUrl} /> <LazyPDFViewer key={documentData.id} documentData={documentData} />
</div> </div>
)} )}
</div> </div>

View File

@ -10,7 +10,7 @@ export default function DocumentSentPage() {
Documents Documents
</Link> </Link>
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl"> <h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
Loading Document... Loading Document...
</h1> </h1>
</div> </div>

View File

@ -6,13 +6,15 @@ import { Edit, Pencil, Share } from 'lucide-react';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link';
import {
TOAST_DOCUMENT_SHARE_ERROR,
TOAST_DOCUMENT_SHARE_SUCCESS,
} from '@documenso/lib/constants/toast';
import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client'; import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
export type DataTableActionButtonProps = { export type DataTableActionButtonProps = {
row: Document & { row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>; User: Pick<User, 'id' | 'name' | 'email'>;
@ -22,16 +24,18 @@ export type DataTableActionButtonProps = {
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => { export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const { data: session } = useSession(); const { data: session } = useSession();
const { toast } = useToast(); const { toast } = useToast();
const [, copyToClipboard] = useCopyToClipboard();
const { createAndCopyShareLink, isCopyingShareLink } = useCopyShareLink({
onSuccess: () => toast(TOAST_DOCUMENT_SHARE_SUCCESS),
onError: () => toast(TOAST_DOCUMENT_SHARE_ERROR),
});
if (!session) { if (!session) {
return null; return null;
} }
const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } =
trpc.shareLink.createOrGetShareLink.useMutation();
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id; const isOwner = row.User.id === session.user.id;
@ -41,20 +45,6 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const isComplete = row.status === DocumentStatus.COMPLETED; const isComplete = row.status === DocumentStatus.COMPLETED;
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const onShareClick = async () => {
const { slug } = await createOrGetShareLink({
token: recipient?.token,
documentId: row.id,
});
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
toast({
title: 'Copied to clipboard',
description: 'The sharing link has been copied to your clipboard.',
});
};
return match({ return match({
isOwner, isOwner,
isRecipient, isRecipient,
@ -80,8 +70,17 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
</Button> </Button>
)) ))
.otherwise(() => ( .otherwise(() => (
<Button className="w-24" loading={isCreatingShareLink} onClick={onShareClick}> <Button
{!isCreatingShareLink && <Share className="-ml-1 mr-2 h-4 w-4" />} className="w-24"
loading={isCopyingShareLink}
onClick={async () =>
createAndCopyShareLink({
token: recipient?.token,
documentId: row.id,
})
}
>
{!isCopyingShareLink && <Share className="-ml-1 mr-2 h-4 w-4" />}
Share Share
</Button> </Button>
)); ));

View File

@ -1,5 +1,7 @@
'use client'; 'use client';
import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { import {
@ -16,11 +18,15 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link';
import {
TOAST_DOCUMENT_SHARE_ERROR,
TOAST_DOCUMENT_SHARE_SUCCESS,
} from '@documenso/lib/constants/toast';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client'; import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client';
import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc as trpcClient } from '@documenso/trpc/client'; import { trpc as trpcClient } from '@documenso/trpc/client';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -30,7 +36,7 @@ import {
} from '@documenso/ui/primitives/dropdown-menu'; } from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard'; import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog';
export type DataTableActionDropdownProps = { export type DataTableActionDropdownProps = {
row: Document & { row: Document & {
@ -41,38 +47,29 @@ export type DataTableActionDropdownProps = {
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => { export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
const { data: session } = useSession(); const { data: session } = useSession();
const { toast } = useToast(); const { toast } = useToast();
const [, copyToClipboard] = useCopyToClipboard();
const { createAndCopyShareLink, isCopyingShareLink } = useCopyShareLink({
onSuccess: () => toast(TOAST_DOCUMENT_SHARE_SUCCESS),
onError: () => toast(TOAST_DOCUMENT_SHARE_ERROR),
});
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
if (!session) { if (!session) {
return null; return null;
} }
const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } =
trpcReact.shareLink.createOrGetShareLink.useMutation();
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id; const isOwner = row.User.id === session.user.id;
// const isRecipient = !!recipient; // const isRecipient = !!recipient;
// const isDraft = row.status === DocumentStatus.DRAFT; const isDraft = row.status === DocumentStatus.DRAFT;
// const isPending = row.status === DocumentStatus.PENDING; // const isPending = row.status === DocumentStatus.PENDING;
const isComplete = row.status === DocumentStatus.COMPLETED; const isComplete = row.status === DocumentStatus.COMPLETED;
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; // const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const isDocumentDeletable = isOwner && row.status === DocumentStatus.DRAFT;
const onShareClick = async () => {
const { slug } = await createOrGetShareLink({
token: recipient?.token,
documentId: row.id,
});
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
toast({
title: 'Copied to clipboard',
description: 'The sharing link has been copied to your clipboard.',
});
};
const onDownloadClick = async () => { const onDownloadClick = async () => {
let document: DocumentWithData | null = null; let document: DocumentWithData | null = null;
@ -112,7 +109,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<MoreHorizontal className="h-5 w-5 text-gray-500" /> <MoreHorizontal className="text-muted-foreground h-5 w-5" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount> <DropdownMenuContent className="w-52" align="start" forceMount>
@ -147,7 +144,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
Void Void
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem disabled> <DropdownMenuItem onClick={() => setDeleteDialogOpen(true)} disabled={!isDocumentDeletable}>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
@ -159,8 +156,16 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
Resend Resend
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={onShareClick}> <DropdownMenuItem
{isCreatingShareLink ? ( disabled={isDraft}
onClick={async () =>
createAndCopyShareLink({
token: recipient?.token,
documentId: row.id,
})
}
>
{isCopyingShareLink ? (
<Loader className="mr-2 h-4 w-4" /> <Loader className="mr-2 h-4 w-4" />
) : ( ) : (
<Share className="mr-2 h-4 w-4" /> <Share className="mr-2 h-4 w-4" />
@ -168,6 +173,14 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
Share Share
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
{isDocumentDeletable && (
<DeleteDraftDocumentDialog
id={row.id}
open={isDeleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
/>
)}
</DropdownMenu> </DropdownMenu>
); );
}; };

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