Compare commits

...

167 Commits

Author SHA1 Message Date
Catalin Pit fe493adbb5 ci: update gh action 2023-09-30 19:48:26 +01:00
Catalin Pit 9437639c53 ci: update gh action 2023-09-30 19:37:54 +01:00
Catalin Pit 2afe8ecef0 ci: update ci command 2023-09-30 19:27:42 +01:00
Catalin Pit fe939d8aa4 ci: fix error no BUILD_ID 2023-09-30 19:18:40 +01:00
Catalin Pit b918e6ba77 ci: update e2e-tests github action 2023-09-30 19:05:59 +01:00
Catalin Pit ae7a372ca0 ci: trigger github action 2023-09-30 18:54:21 +01:00
Catalin Pit a18932378f ci: update GitHub Actions 2023-09-30 10:52:58 +01:00
pit 5f27eb20fd chore: trigger ci 2023-09-27 12:08:22 +01:00
pit 0d91e61a50 chore: trigger ci 2023-09-27 11:26:17 +01:00
pit a2322e68d7 chore: trigger ci 2023-09-27 11:19:21 +01:00
pit acb1fde5e7 feat: add docker to gh action 2023-09-27 11:17:17 +01:00
pit f057666776 chore: e2e-tests GitHub Action update 2023-09-27 10:11:32 +01:00
pit e82c12154b chore: e2e-tests GitHub Action update 2023-09-27 10:02:02 +01:00
pit 7efa8f0fc8 chore: e2e-tests GitHub action change 2023-09-27 10:01:02 +01:00
pit 63c2c4acf4 chore: implemented feedback 2023-09-27 09:51:30 +01:00
pit 5c81105e71 chore: merged feat/refresh 2023-09-27 09:16:20 +01:00
Mythie 7bd847c0d3 chore: add missing migrations 2023-09-27 16:53:48 +10:00
Lucas Smith 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
Mythie 572f9d5ad6 chore: styling updates 2023-09-27 14:58:10 +10:00
Mythie e49c102e8c fix: lint errors 2023-09-27 09:11:56 +10:00
Mythie 2348221b03 fix: faster tooltips 2023-09-27 09:09:53 +10:00
Mythie a0c327cfcf fix: share og updates 2023-09-27 09:03:53 +10:00
Mythie ddbeb9e3db fix: twitter images 2023-09-27 08:02:51 +10:00
Mythie e8205c1390 fix: better share links 2023-09-27 07:52:24 +10:00
Timur Ercan fbb53fc8c3 chore: update mania shirt res 2023-09-26 22:06:41 +02:00
Timur Ercan d7fed5a5dc feat: shop Article 2023-09-26 21:49:04 +02:00
pit 11fce3ffba chore: merged feat/refresh 2023-09-26 16:27:11 +01:00
pit 86dddb8a4d feat: improve tests 2023-09-26 16:19:26 +01:00
pit fba38f7b02 chore: cleanup 2023-09-26 12:19:59 +01:00
pit 2e2ef4c20b chore: updated GitHub Action 2023-09-26 11:16:33 +01:00
Timur Ercan 3285881586 chore: typos 2023-09-26 11:39:30 +02:00
Timur Ercan 3ff61607a2 Merge pull request #370 from documenso/feat/mania
feat: malfunction mania blog post
2023-09-26 11:24:24 +02:00
Timur Ercan 2a124b03e9 Merge branch 'feat/refresh' into feat/mania 2023-09-26 11:24:13 +02:00
Timur Ercan e1aa23bc55 chore: fix typo 2023-09-26 11:23:39 +02:00
Timur Ercan 818c5c7ba4 chore: fix lightmode logo 2023-09-26 11:19:32 +02:00
Timur Ercan 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
Timur Ercan 562fd043a9 chore: typo 2023-09-26 10:31:41 +02:00
Timur Ercan b4781c011c chore: alt text 2023-09-26 10:27:19 +02:00
Timur Ercan 5c7d3d5503 chore: update date 2023-09-26 10:26:16 +02:00
Timur Ercan 0f2b6c0ebf chore: remove unused image 2023-09-26 10:23:28 +02:00
Timur Ercan ab5bdfeae4 chore: typo 2023-09-26 10:18:56 +02:00
Mythie 2d2615447e fix: redirectless authentication 2023-09-26 16:17:01 +10:00
Mythie 46465acd73 chore: quieten dependabot 2023-09-26 16:11:06 +10:00
Lucas Smith 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
Mythie 584d5bd8ea fix: update share preview 2023-09-26 15:58:43 +10:00
Mythie e4b6d42672 fix: styling updates 2023-09-26 15:56:11 +10:00
Timur Ercan 109ad190cf fix: text 2023-09-25 19:13:03 +02:00
Ephraim Atta-Duncan 78498793fa chore: refactor function 2023-09-25 11:21:39 +00:00
Ephraim Atta-Duncan b2e916378d feat: add dropdown to tweet or copy signing link 2023-09-25 11:15:31 +00:00
Mythie a52c19355a chore: sign document 2023-09-25 15:57:10 +10:00
Mythie e67e96333b fix: dark mode for generic mdx content 2023-09-25 11:31:26 +10:00
Lucas Smith 7aa75b0c64 Merge pull request #403 from documenso/feat/single-player-mode
feat: single player mode
2023-09-25 11:25:58 +10:00
Mythie 9b1069f208 fix: remove usage of buffer 2023-09-25 11:24:55 +10:00
Mythie abe20b8dcf fix: assorted updates 2023-09-25 11:17:59 +10:00
Mythie 58509c54a9 fix: feature flag client endpoint 2023-09-25 10:35:18 +10:00
Mythie fd545cee0c chore: update env.example 2023-09-25 10:33:38 +10:00
Mythie 1f92bffe7d chore: remove console.log 2023-09-25 10:09:02 +10:00
Timur Ercan 5615627001 Merge pull request #419 from documenso/fix/incorporation
fix: company name
2023-09-24 21:53:10 +02:00
Timur Ercan 78c55875ef fix: company name 2023-09-24 21:52:09 +02:00
Mythie a3674947b8 fix: single player dark mode and animation updates 2023-09-25 00:18:41 +10:00
Mythie 63cc57b035 fix: improve dark mode background patterns 2023-09-25 00:18:15 +10:00
Mythie d4bc1eb0d1 fix: cors for feature flags 2023-09-25 00:16:01 +10:00
Mythie c9f5496acb Merge remote-tracking branch 'origin/feat/refresh' into feat/single-player-mode 2023-09-24 22:18:01 +10:00
Mythie 99481b6144 feat: darker dark theme 2023-09-24 14:45:50 +10:00
Mythie cee147bc9a fix: normalize recipients 2023-09-24 11:46:36 +10:00
Lucas Smith 3a825391b9 Merge pull request #399 from captain-Akshay/feat/handle_click
fix: cancel button handler
2023-09-23 22:19:57 +10:00
Lucas Smith bb347e4614 Merge pull request #326 from documenso/feat/completed-share-link
feat: signing completed sharing link
2023-09-23 22:18:17 +10:00
Lucas Smith 68d624ac78 Merge branch 'feat/refresh' into feat/completed-share-link 2023-09-23 21:07:31 +10:00
David Nguyen 8012d665ae fix: firefox signing fields 2023-09-23 13:25:39 +10:00
Timur Ercan fde53e355f chore: staging 2023-09-22 18:44:34 +02:00
Timur Ercan 8e0a10298e Merge branch 'feat/refresh' into feat/mania 2023-09-22 18:33:16 +02:00
Lucas Smith 1e5ecd79c2 Merge pull request #354 from documenso/feat/table-empty-state
feat: add empty document state
2023-09-22 23:36:42 +10:00
Lucas Smith 58f10268e2 Merge pull request #359 from documenso/feat/send-email
feat: send email when recipient is done signing and every recipient is done signing
2023-09-22 23:29:35 +10:00
Lucas Smith 43ce76d928 fix: remove unused import 2023-09-22 23:29:14 +10:00
Lucas Smith 201ba65e1c fix: remove unused import 2023-09-22 23:23:55 +10:00
Lucas Smith 0d130b17c8 fix: minor updates 2023-09-22 23:22:48 +10:00
Lucas Smith 2c90f767fd Merge branch 'feat/refresh' into feat/send-email 2023-09-22 23:19:43 +10:00
Lucas Smith 946b2fe129 fix: use named export 2023-09-22 23:15:30 +10:00
Lucas Smith 2a2dbb65c6 Merge branch 'feat/refresh' into feat/table-empty-state 2023-09-22 23:13:51 +10:00
Lucas Smith ef2300a600 Merge pull request #343 from adithyaakrishna/feat/404
feat: added custom `not-found` or 404 pages
2023-09-22 23:11:34 +10:00
Lucas Smith eea99ac871 Merge pull request #391 from documenso/feat/custom-emails
feat: send custom emails
2023-09-22 22:53:09 +10:00
pit 1b31d590a5 chore: use fixtures 2023-09-22 13:45:59 +01:00
Mythie f4c7799537 fix: reverse meta relation and tidy code 2023-09-22 12:42:06 +00:00
David Nguyen 764c6b88c5 feat: update success page text 2023-09-22 16:35:49 +10:00
David Nguyen 989d7a351f feat: add uninserted field validation 2023-09-22 16:25:09 +10:00
Lucas Smith 78325d9b39 Merge branch 'feat/refresh' into feat/404 2023-09-22 15:01:49 +10:00
captain-Akshay 679fb80f9a fix: cancel button handler 2023-09-22 10:31:42 +05:30
David Nguyen dc49277bf9 feat: add uninserted field validation 2023-09-22 12:27:39 +10:00
pit a882b72492 chore: removed auth 2023-09-21 15:57:11 +01:00
pit 03fa6a3fa9 chore: add auth to gitignore 2023-09-21 15:56:23 +01:00
pit bcf1ffabd4 chore: removed test details from env file 2023-09-21 15:55:05 +01:00
pit 8eff8db990 chore: changed files 2023-09-21 15:54:02 +01:00
pit cbb56891c4 feat: moved playwright to the web app 2023-09-21 15:52:53 +01:00
Lucas Smith b3f26055d9 Merge pull request #395 from nsylke/nsylke-patch-10
fix: remove disallow property of _next from robots
2023-09-21 22:36:30 +10:00
Mythie 1b276a0469 fix: support optimise imports 2023-09-21 21:34:24 +10:00
Mythie 17fbf2673c Merge remote-tracking branch 'origin/feat/refresh' into feat/completed-share-link 2023-09-21 18:55:52 +10:00
Mythie cdc50ec876 fix: resolve issues with open graph asset loading 2023-09-21 05:37:04 +00:00
nsylke 181af24b78 fix: remove disallow property of _next from robots 2023-09-20 20:46:23 -05:00
Mythie 4b13a42731 fix: tidy code and update endpoints 2023-09-21 00:51:02 +00:00
Ephraim Atta-Duncan e330e90688 fix: document meta relation with document 2023-09-20 12:54:24 +00:00
Ephraim Atta-Duncan f2d3c51651 fix: avoid creating document meta with empty strings 2023-09-20 12:38:39 +00:00
Ephraim Atta-Duncan c9c111cdf2 fix: persist newline in emails 2023-09-20 12:19:52 +00:00
Lucas Smith c247295131 Merge pull request #388 from captain-Akshay/feat/theme
feat: added a better theme change ability for user
2023-09-20 21:45:45 +10:00
Ephraim Atta-Duncan d417255910 feat: replace template variables with values
Co-authored-by: Mythie <me@lucasjamessmith.me>
2023-09-20 11:17:26 +00:00
Ephraim Atta-Duncan 677a15327b feat: send custom email message 2023-09-20 09:59:42 +00:00
Ephraim Atta-Duncan d5238939ad feat: persist document metadata in database for a specific user 2023-09-20 09:51:04 +00:00
Ephraim Atta-Duncan 6860726e83 feat: send custom email subjects 2023-09-20 09:06:28 +00:00
Ephraim Atta-Duncan a55197fb2d feat: add prisma schema for document meta 2023-09-20 08:57:50 +00:00
David Nguyen 7eed5c7c96 feat: utilise transport layer 2023-09-20 15:55:25 +10:00
David Nguyen 1c629af651 fix: timeout issues 2023-09-20 15:33:01 +10:00
Lucas Smith bcb163693a Merge branch 'feat/refresh' into feat/completed-share-link 2023-09-20 12:42:30 +10:00
captain-Akshay 7771d7acbe feat: added the icon for theme 2023-09-20 06:56:40 +05:30
captain-Akshay 4cd56fa98c feat: removed unecessary code to the file and made few UI changes 2023-09-19 19:45:21 +05:30
pit 5d5c5b2748 chore: merged feat/refresh 2023-09-19 11:47:25 +01:00
pit 97b4655bc8 feat: improve tests 2023-09-19 11:45:12 +01:00
captain-Akshay a1ce6f696a feat: added a better theme change ability for user 2023-09-19 13:38:37 +05:30
pit 3c43d7af05 chore: improved structure'
'
2023-09-18 21:13:55 +01:00
Timur Ercan eaa3e0a303 Merge branch 'feat/refresh' into feat/mania 2023-09-18 20:15:50 +02:00
Timur Ercan a283c88d7f chore: update malfunction mania image 2023-09-18 20:11:27 +02:00
pit 5beb1c6192 feat: fix auth issue 2023-09-18 11:32:21 +01:00
Ephraim Atta-Duncan c2a9647c90 Merge branch 'feat/refresh' into feat/redirect-signed-document 2023-09-17 14:52:44 +00:00
Ephraim Atta-Duncan 02424596db fix: update for code review 2023-09-17 14:45:22 +00:00
Ephraim Atta-Duncan 776324c875 fix: fitler only unsigned documents 2023-09-17 14:38:39 +00:00
Ephraim Atta-Duncan 6f4c280583 chore: match file name and method name 2023-09-17 14:31:44 +00:00
Catalin Pit ba31138217 feat: add more tests 2023-09-15 15:57:18 +03:00
Catalin Pit 7a9d987258 chore: removed auth 2023-09-14 18:08:54 +03:00
Catalin Pit b0592c5d00 chore: remove auth from pr 2023-09-14 18:05:59 +03:00
Catalin Pit aee103d4e3 chore: deleted file 2023-09-14 18:04:50 +03:00
Catalin Pit 0148c0f0cf feat: add-playwright 2023-09-14 18:03:35 +03:00
Timur Ercan 5809109c05 chore: grammerly, finetuned bounties 2023-09-14 15:43:17 +02:00
Timur Ercan a6400eb6c9 Merge branch 'feat/refresh' into feat/mania 2023-09-14 15:29:55 +02:00
flō 39958ed22c Fix typo 2023-09-14 14:57:11 +02:00
flō 7c4ba1b1ea Fix typo 2023-09-14 11:38:42 +02:00
flō f588897531 Fix punctuation for consistency 2023-09-14 11:33:11 +02:00
flō 5629e08f83 Add keywords
added keywords in description for SEO optimizations
2023-09-14 11:06:48 +02:00
flō 37394c054c Fix typo
Fix typo and minor edits for consistency
2023-09-14 11:04:10 +02:00
Timur Ercan b7c0df67b1 feat: malfunction mania, first draft 2023-09-11 18:20:17 +02:00
Lucas Smith 2bad1b9f55 fix: tidy messaging 2023-09-09 03:45:15 +00:00
Lucas Smith 73b0dc315e fix: use ts-pattern 2023-09-09 03:31:17 +00:00
Ephraim Atta-Duncan 525ff21563 feat: avoid sending pending email to document with 1 recipients 2023-09-07 20:52:18 +00:00
Ephraim Atta-Duncan 863e53a2d5 refactor: pass document id as arguments 2023-09-07 20:38:18 +00:00
Ephraim Atta-Duncan da2033692c feat: send email when all recipients have signed 2023-09-07 20:14:04 +00:00
Ephraim Atta-Duncan dbbe17a0a8 feat: send email when recipient is done signing 2023-09-07 19:58:48 +00:00
Ephraim Atta-Duncan 0c145fab0b chore: fix eslint errors 2023-09-06 12:01:30 +00:00
Ephraim Atta-Duncan f6e49e3f21 chore: remove code from different branch 2023-09-06 12:00:23 +00:00
Ephraim Atta-Duncan 1f027d75d3 chore: fix eslint errors 2023-09-06 11:55:02 +00:00
Ephraim Atta-Duncan 1ba7767f8e chore: correct types on component 2023-09-06 11:52:15 +00:00
Ephraim Atta-Duncan 8220b2f086 feat: add empty state for different status 2023-09-06 11:47:58 +00:00
Ephraim Atta-Duncan a74374e39f feat: add initial empty state for no results 2023-09-06 11:11:13 +00:00
Ephraim Atta-Duncan e47ab838c5 chore: remove undefined check 2023-09-06 07:37:03 +00:00
Ephraim Atta-Duncan 551918ab9b feat: redirect signed document to completed page 2023-09-05 13:53:18 +00:00
Ephraim Atta-Duncan 739f3763dd feat: update share page to match latest changes 2023-09-05 11:40:42 +00:00
Ephraim Atta-Duncan 0cdd980a4b feat: move opengraph-image to next.js 13 implementation 2023-09-05 11:37:21 +00:00
Ephraim Atta-Duncan b43ddcbea2 refactor: redirect using useRouter 2023-09-05 09:29:47 +00:00
Ephraim Atta-Duncan 75feaefbf2 chore: remove unused files 2023-09-05 09:27:04 +00:00
Mythie d524ea77ab fix: update styling 2023-09-05 13:15:45 +10:00
Adithya Krishna 2524458b0c chore: updated wording
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-09-03 23:59:02 +05:30
Adithya Krishna 12c45fb882 feat: added 404 page for web app
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-09-03 23:53:07 +05:30
Adithya Krishna 118483b6cc chore: updated 404 page for marketing app
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-09-03 23:52:51 +05:30
Adithya Krishna fd6350b397 feat: added 404 page for marketing app
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-09-03 23:30:48 +05:30
Mythie 8d4df7d3dd fix: update og card 2023-08-31 15:36:09 +10:00
Lucas Smith f1f6e2e40a Merge branch 'feat/refresh' into feat/completed-share-link 2023-08-31 14:20:52 +10:00
Ephraim Atta-Duncan 66dfdc5ad0 feat: redirect share page to marketing page after 3 seconds 2023-08-29 19:42:37 +00:00
Ephraim Atta-Duncan b45978374b feat: generate metatags for share page with og image 2023-08-29 19:08:54 +00:00
Ephraim Atta-Duncan 81c3e701e2 feat: display signature text from params 2023-08-29 18:46:15 +00:00
Ephraim Atta-Duncan 874d919a6a feat: create sharing id for each recipient 2023-08-29 18:23:52 +00:00
Ephraim Atta-Duncan e8559cecd5 feat: add initial og image for share link 2023-08-29 14:39:49 +00:00
170 changed files with 4050 additions and 1020 deletions
+15 -4
View File
@@ -6,18 +6,29 @@ NEXTAUTH_SECRET="secret"
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
# [[APP]]
# [[URLS]]
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
# [[MARKETING]]
NEXT_PUBLIC_MARKETING_SITE_URL="http://localhost:3001"
# [[DATABASE]]
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
# 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"
# [[E2E Tests]]
E2E_TEST_SIGNER_NAME="Test Signer"
E2E_TEST_SIGNER_EMAIL="testsigner@mail.com"
E2E_TEST_SIGNING_SUBJECT="Test subject"
E2E_TEST_SIGNING_MESSAGE="Test message"
# User for the "auth.setup.ts" file and the authenticated tests
E2E_TEST_AUTHENTICATE_USERNAME="New user"
E2E_TEST_AUTHENTICATE_USER_EMAIL="mytestnewuser@mail.com"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="new_test_password"
# User for the *.unauthenticated.ts files
E2E_TEST_USERNAME="Test"
E2E_TEST_USER_EMAIL="mytestuser@mail.com"
E2E_TEST_USER_PASSWORD="test_password"
# [[STORAGE]]
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
NEXT_PUBLIC_UPLOAD_TRANSPORT="database"
+3 -3
View File
@@ -9,7 +9,7 @@ updates:
labels:
- "ci dependencies"
- "ci"
open-pull-requests-limit: 10
open-pull-requests-limit: 2
- package-ecosystem: "npm"
directory: "/apps/marketing"
@@ -19,7 +19,7 @@ updates:
labels:
- "npm dependencies"
- "frontend"
open-pull-requests-limit: 10
open-pull-requests-limit: 2
- package-ecosystem: "npm"
directory: "/apps/web"
@@ -29,4 +29,4 @@ updates:
labels:
- "npm dependencies"
- "frontend"
open-pull-requests-limit: 10
open-pull-requests-limit: 2
+53
View File
@@ -0,0 +1,53 @@
name: Playwright Tests
on:
push:
branches: [feat/refresh, feat/add-e2e-testing]
pull_request:
branches: [feat/refresh, feat/add-e2e-testing]
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
env:
NEXT_PRIVATE_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
NEXT_PRIVATE_DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Generate package-lock.json
run: npm cache clean --force && npm install
- 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: Install Prisma Client
run: npm install @prisma/client
- name: Generate Prisma Client
run: npm run prisma:generate -w @documenso/prisma
- name: Create the database
run: npm run prisma:migrate-dev -w @documenso/prisma
- 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
+5
View File
@@ -47,3 +47,8 @@ yarn-error.log*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
**/test-results/
/playwright-report/
/playwright/.cache/
playwright/.auth
+14 -21
View File
@@ -1,6 +1,6 @@
<p align="center" style="margin-top: 120px">
<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">
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/ea8de2d0-8e33-4527-8a5c-40780142a2ed" alt="Documenso Logo">
</a>
<p align="center">
@@ -26,37 +26,30 @@
<a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
</p>
> **🚧 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>
<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%"
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%"
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%"
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%"
src="https://user-images.githubusercontent.com/1309312/224570322-b2c76ea8-7482-4043-ad97-f1221220c591.png">
<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">
src=https://github.com/documenso/documenso/assets/1309312/c0f55116-ab82-433f-a266-f3fc8571d69f">
</div>
> **Note**
> This project is currently under community review and will publish it's first production release soon™.
## 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 is 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
@@ -65,13 +58,13 @@ Signing documents digitally is fast, easy and should be best practice for every
## 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
- Tell us what you think in the current [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
- ⭐ 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
## Contributing
@@ -0,0 +1,54 @@
---
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 inbroken 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.
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.**
+2 -2
View File
@@ -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 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 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:
+62
View File
@@ -0,0 +1,62 @@
---
title: Shop and Limited Edition "Mania" Shirt
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:
- Testing
- 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 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 the limited edition shirt for Malfunction Mania. 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.
<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 to get 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 documenso
- ...
**[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.**
Binary file not shown.
+2 -1
View File
@@ -8,6 +8,7 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"clean": "rimraf .next && rimraf node_modules",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
@@ -18,7 +19,7 @@
"@hookform/resolvers": "^3.1.0",
"contentlayer": "^0.3.4",
"framer-motion": "^10.12.8",
"lucide-react": "^0.277.0",
"lucide-react": "^0.279.0",
"micro": "^10.0.1",
"next": "13.4.19",
"next-auth": "4.22.3",
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: 956 KiB

@@ -39,7 +39,7 @@ export default function ContentPage({ params }: { params: { content: string } })
const MDXContent = useMDXComponent(post.body.code);
return (
<article className="prose prose-slate mx-auto">
<article className="prose dark:prose-invert mx-auto">
<MDXContent components={mdxComponents} />
</article>
);
@@ -39,21 +39,21 @@ export default function BlogPostPage({ params }: { params: { post: string } }) {
const MDXContent = useMDXComponent(post.body.code);
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">
<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()}
</time>
<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="h-10 w-10 rounded-full bg-gray-50">
<div className="bg-foreground h-10 w-10 rounded-full">
{post.authorImage && (
<img
src={post.authorImage}
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>
@@ -13,13 +13,13 @@ export default function BlogPage() {
<div className="text-center">
<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
more!
</p>
</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) => (
<article
key={`blog-${i}`}
@@ -57,12 +57,12 @@ export default function BlogPage() {
</div>
<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 && (
<img
src={post.authorImage}
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>
@@ -58,40 +58,40 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
return (
<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{' '}
<u>{user.name}</u>
</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.
</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
want to make the world a better place.
</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,
and more secure. And we're going to do it together.
</p>
<div className="mt-12">
<h2 className="text-2xl font-bold text-slate-900">Let's do it together</h2>
<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="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none">
<p
className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl',
'text-foreground text-4xl font-semibold md:text-5xl',
fontCaveat.className,
)}
>
Timur
</p>
<p className="text-sm text-slate-500 md:text-lg">
<p className="text-muted-foreground text-sm md:text-lg">
Timur Ercan
<span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span>
@@ -102,14 +102,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">
<p
className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl',
'text-foreground text-4xl font-semibold md:text-5xl',
fontCaveat.className,
)}
>
Lucas
</p>
<p className="text-sm text-slate-500 md:text-lg">
<p className="text-muted-foreground text-sm md:text-lg">
Lucas Smith
<span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span>
@@ -119,12 +119,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">
{signatureDataUrl && (
<img src={signatureDataUrl} alt="your-signature" className="max-w-[172px]" />
<img
src={signatureDataUrl}
alt="your-signature"
className="max-w-[172px] dark:invert"
/>
)}
{!signatureDataUrl && (
<p
className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl',
'text-foreground text-4xl font-semibold md:text-5xl',
fontCaveat.className,
)}
>
@@ -132,7 +136,7 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
</p>
)}
<p className="text-sm text-slate-500 md:text-lg">
<p className="text-muted-foreground text-sm md:text-lg">
{user.name}
<span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span>
@@ -143,20 +147,20 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
</div>
<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">
<p className="text-lg text-slate-500">
<p className="text-muted-foreground text-lg">
<span className="font-bold">Email:</span> {user.email}
</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>{' '}
<PasswordReveal password={password ?? 'password'} />
</p>
</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.
</p>
+23 -3
View File
@@ -1,4 +1,8 @@
import React from 'react';
'use client';
import React, { useEffect, useState } from 'react';
import { cn } from '@documenso/ui/lib/utils';
import { Footer } from '~/components/(marketing)/footer';
import { Header } from '~/components/(marketing)/header';
@@ -8,15 +12,31 @@ export type MarketingLayoutProps = {
};
export default function MarketingLayout({ children }: MarketingLayoutProps) {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const onScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<div className="relative max-w-[100vw] overflow-y-auto overflow-x-hidden pt-20 md:pt-28">
<div className="fixed left-0 top-0 z-50 w-full bg-white/50 backdrop-blur-md">
<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" />
</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>
);
}
@@ -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>
);
};
@@ -1,152 +1,23 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { Variants, motion } from 'framer-motion';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
import { z } from 'zod';
import backgroundPattern from '~/assets/background-pattern.png';
const OSSFriends = [
{
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',
},
];
import { OSSFriendsContainer } from './container';
import { TOSSFriendsSchema, ZOSSFriendsSchema } from './schema';
const ContainerVariants: Variants = {
initial: {
opacity: 0,
},
animate: {
opacity: 1,
transition: {
staggerChildren: 0.075,
export default async function OSSFriendsPage() {
const ossFriends: TOSSFriendsSchema = await fetch('https://formbricks.com/api/oss-friends', {
next: {
revalidate: 3600,
},
},
};
})
.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 (
<div className="relative mt-12">
<div className="text-center">
@@ -154,49 +25,19 @@ export default function OSSFriendsPage() {
Our <span title="Open Source Software">OSS</span> Friends
</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
just as passionate about open source as we are.
</p>
</div>
<motion.div
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>
<OSSFriendsContainer className="mt-12" ossFriends={ossFriends} />
<div className="absolute inset-0 -z-10 flex items-start justify-center">
<Image
src={backgroundPattern}
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>
@@ -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>;
@@ -24,10 +24,10 @@ export default function PricingPage() {
<div className="text-center">
<h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1>
<p className="mt-4 text-lg leading-normal text-[#31373D]">
<p className="text-foreground mt-4 text-lg leading-normal">
Designed for every stage of your journey.
</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 className="mt-12">
@@ -45,7 +45,7 @@ export default function PricingPage() {
What is the difference between the plans?
</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
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
@@ -59,7 +59,7 @@ export default function PricingPage() {
How do you handle my data?
</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
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.
@@ -71,7 +71,7 @@ export default function PricingPage() {
Why should I use your hosting service?
</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
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
@@ -84,7 +84,7 @@ export default function PricingPage() {
How can I contribute?
</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{' '}
<Link
className="text-documenso-700 font-bold"
@@ -111,7 +111,7 @@ export default function PricingPage() {
Can I use Documenso commercially?
</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
can use it for free and even modify it to fit your needs, as long as you publish your
changes under the same license.
@@ -123,7 +123,7 @@ export default function PricingPage() {
Why should I prefer Documenso over DocuSign or some other signing tool?
</AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
<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,
everybody is free to use and adapt. By being truly open we want to create trusted
infrastructure for the future of the internet.
@@ -135,7 +135,7 @@ export default function PricingPage() {
Where can I get support?
</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{' '}
<Link
className="text-documenso-700 font-bold"
@@ -1,8 +1,9 @@
import { notFound } from 'next/navigation';
import { getDocumentAndRecipientByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { DocumentStatus } from '@documenso/prisma/client';
import SinglePlayerModeSuccess from '~/components/(marketing)/single-player-mode/single-player-mode-success';
import { SinglePlayerModeSuccess } from '~/components/(marketing)/single-player-mode/single-player-mode-success';
export type SinglePlayerModeSuccessPageProps = {
params: {
@@ -21,7 +22,7 @@ export default async function SinglePlayerModeSuccessPage({
token,
}).catch(() => null);
if (!document || document.status !== 'COMPLETED') {
if (!document || document.status !== DocumentStatus.COMPLETED) {
return notFound();
}
@@ -6,6 +6,8 @@ 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 { Field, Prisma, Recipient } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
@@ -31,7 +33,7 @@ export default function SinglePlayerModePage() {
const { toast } = useToast();
const [uploadedFile, setUploadedFile] = useState<{ name: string; file: string } | null>();
const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>();
const [step, setStep] = useState<SinglePlayerModeStep>('fields');
const [fields, setFields] = useState<Field[]>([]);
@@ -97,7 +99,7 @@ export default function SinglePlayerModePage() {
};
/**
* Create, sign and send the document.
* Upload, create, sign and send the document.
*/
const onSignSubmit = async (data: TAddSignatureFormSchema) => {
if (!uploadedFile) {
@@ -105,9 +107,14 @@ export default function SinglePlayerModePage() {
}
try {
const putFileData = await putFile(uploadedFile.file);
const documentToken = await createSinglePlayerDocument({
document: uploadedFile.file,
documentName: uploadedFile.name,
documentData: {
type: putFileData.type,
data: putFileData.data,
},
documentName: uploadedFile.file.name,
signer: data,
fields: fields.map((field) => ({
page: field.page,
@@ -149,11 +156,11 @@ export default function SinglePlayerModePage() {
const onFileDrop = async (file: File) => {
try {
const arrayBuffer = await file.arrayBuffer();
const base64String = Buffer.from(arrayBuffer).toString('base64');
const base64String = base64.encode(new Uint8Array(arrayBuffer));
setUploadedFile({
name: file.name,
file: `data:application/pdf;base64,${base64String}`,
file,
fileBase64: `data:application/pdf;base64,${base64String}`,
});
analytics.capture('Marketing: SPM - Document uploaded');
@@ -171,12 +178,12 @@ export default function SinglePlayerModePage() {
<div className="text-center">
<h1 className="text-3xl font-bold lg:text-5xl">Single Player Mode</h1>
<p className="mt-4 text-lg leading-normal text-[#31373D]">
<p className="text-foreground mx-auto mt-4 max-w-[50ch] text-lg leading-normal">
View our{' '}
<Link
href={'/pricing'}
target="_blank"
className="font-semibold transition-colors hover:text-[#31373D]/80"
className="hover:text-foreground/80 font-semibold transition-colors"
>
community plan
</Link>{' '}
@@ -189,7 +196,7 @@ export default function SinglePlayerModePage() {
{uploadedFile ? (
<Card gradient>
<CardContent className="p-2">
<LazyPDFViewer document={uploadedFile.file} />
<LazyPDFViewer document={uploadedFile.fileBase64} />
</CardContent>
</Card>
) : (
@@ -198,7 +205,7 @@ export default function SinglePlayerModePage() {
</div>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
<DocumentFlowFormContainer onSubmit={(e) => e.preventDefault()}>
<DocumentFlowFormContainer className="top-24" onSubmit={(e) => e.preventDefault()}>
<DocumentFlowFormContainerHeader
title={currentDocumentFlow.title}
description={currentDocumentFlow.description}
+6 -2
View File
@@ -7,6 +7,7 @@ import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag'
import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster';
import { ThemeProvider } from '~/providers/next-theme';
import { PlausibleProvider } from '~/providers/plausible';
import { PostHogPageview } from '~/providers/posthog';
@@ -61,9 +62,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<body>
<FeatureFlagProvider initialFlags={flags}>
<PlausibleProvider>{children}</PlausibleProvider>
<Toaster />
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<PlausibleProvider>{children}</PlausibleProvider>
</ThemeProvider>
</FeatureFlagProvider>
<Toaster />
</body>
</html>
);
+65
View File
@@ -0,0 +1,65 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { ChevronLeft } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import backgroundPattern from '~/assets/background-pattern.png';
export default function NotFound() {
const router = useRouter();
return (
<div className={cn('relative max-w-[100vw] overflow-hidden')}>
<div className="absolute -inset-24 -z-10">
<motion.div
className="flex h-full w-full items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 0.8, transition: { duration: 0.5, delay: 0.5 } }}
>
<Image
src={backgroundPattern}
alt="background pattern"
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
/>
</motion.div>
</div>
<div className="container mx-auto flex h-full min-h-screen items-center px-6 py-32">
<div>
<p className="text-muted-foreground font-semibold">404 Page not found</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
<p className="text-muted-foreground mt-4 text-sm">
The page you are looking for was moved, removed, renamed or might never have existed.
</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button
variant="ghost"
className="w-32"
onClick={() => {
void router.back();
}}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Go Back
</Button>
<Button className="w-32" asChild>
<Link href="/">Home</Link>
</Button>
</div>
</div>
</div>
</div>
);
}
+5 -5
View File
@@ -4,11 +4,11 @@ import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/*',
disallow: ['/_next/*'],
},
rules: [
{
userAgent: '*',
},
],
sitemap: `${getBaseUrl()}/sitemap.xml`,
};
}
@@ -41,7 +41,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
onClick={onSignUpClick}
>
Get the Community 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!
</span>
</Button>
@@ -55,7 +55,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
<Github className="mr-2 h-5 w-5" />
Star on Github
{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')}
</span>
)}
@@ -123,7 +123,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
)}
<div>
<Label className="text-slate-500">Name</Label>
<Label className="text-muted-foreground">Name</Label>
<Input type="text" className="mt-2" {...register('name')} autoFocus />
@@ -131,7 +131,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
</div>
<div>
<Label className="text-slate-500">Email</Label>
<Label className="text-muted-foreground">Email</Label>
<Input type="email" className="mt-2" {...register('email')} />
@@ -22,7 +22,7 @@ export const FasterSmarterBeautifulBento = ({
<Image
src={backgroundPattern}
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>
<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">
<Card className="col-span-2" degrees={45} gradient>
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
<p className="col-span-12 leading-relaxed text-[#555E67] lg:col-span-6">
<p className="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
<strong className="block">Fast.</strong>
When it comes to sending or receiving a contract, you can count on lightning-fast
speeds.
</p>
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
<Image src={cardFastFigure} alt="its fast" className="max-w-[80%] lg:max-w-none" />
<Image
src={cardFastFigure}
alt="its fast"
className="max-w-[80%] dark:contrast-[70%] dark:hue-rotate-180 dark:invert lg:max-w-none"
/>
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<p className="text-foreground/80 leading-relaxed">
<strong className="block">Beautiful.</strong>
Because signing should be celebrated. Thats why we care about the smallest detail in
our product.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardBeautifulFigure} alt="its fast" className="w-full max-w-xs" />
<Image
src={cardBeautifulFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<p className="text-foreground/80 leading-relaxed">
<strong className="block">Smart.</strong>
Our custom templates come with smart rules that can help you save time and energy.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardSmartFigure} alt="its fast" className="w-full max-w-[16rem]" />
<Image
src={cardSmartFigure}
alt="its fast"
className="w-full max-w-[16rem] dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div>
</CardContent>
</Card>
@@ -1,9 +1,12 @@
'use client';
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { Github, MessagesSquare, Twitter } from 'lucide-react';
import { Github, MessagesSquare, Moon, Sun, Twitter } from 'lucide-react';
import { useTheme } from 'next-themes';
import { cn } from '@documenso/ui/lib/utils';
@@ -27,17 +30,30 @@ const FOOTER_LINKS = [
];
export const Footer = ({ className, ...props }: FooterProps) => {
const { setTheme } = useTheme();
return (
<div className={cn('border-t py-12', className)} {...props}>
<div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8">
<div>
<Link href="/">
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
<Image
src="/logo.png"
alt="Documenso Logo"
className="dark:invert"
width={170}
height={0}
/>
</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) => (
<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>
))}
@@ -50,17 +66,29 @@ export const Footer = ({ className, ...props }: FooterProps) => {
key={index}
href={link.href}
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>
))}
</div>
</div>
<div className="mx-auto mt-4 w-full max-w-screen-xl px-8 md:mt-12 lg:mt-24">
<p className="text-sm text-[#8D8D8D]">
<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-muted-foreground text-sm">
© {new Date().getFullYear()} Documenso, Inc. All rights reserved.
</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>
);
@@ -24,13 +24,19 @@ export const Header = ({ className, ...props }: HeaderProps) => {
<header className={cn('flex items-center justify-between', className)} {...props}>
<div className="flex items-center space-x-4">
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
<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>
{isSinglePlayerModeMarketingEnabled && (
<Link
href="/single-player-mode"
className="bg-primary rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
className="bg-primary dark:text-background rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
>
Try now!
</Link>
@@ -38,22 +44,31 @@ export const Header = ({ className, ...props }: HeaderProps) => {
</div>
<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
</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
</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
</Link>
<Link
href="https://app.documenso.com/login"
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
</Link>
@@ -86,7 +86,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
<Image
src={backgroundPattern}
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>
</div>
@@ -115,7 +115,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
onClick={onSignUpClick}
>
Get the Community 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!
</span>
</Button>
@@ -134,11 +134,16 @@ export const Hero = ({ className, ...props }: HeroProps) => {
variants={HeroTitleVariants}
initial="initial"
animate="animate"
className="border-primary mx-auto mt-8 w-fit rounded-xl border-2 bg-white transition-colors hover:bg-slate-50/60"
className="border-primary bg-background hover:bg-muted mx-auto mt-8 w-60 rounded-xl border transition duration-300"
>
<Link href="/single-player-mode" className="block px-4 py-2 text-center">
<h2 className="text-xs font-semibold text-[#727272]">Single Player Mode</h2>
<h1 className="font-semibold leading-5 text-[#606060]">Self sign documents here</h1>
<h2 className="text-muted-foreground text-xs font-semibold">
Introducing Single Player Mode
</h2>
<h1 className="text-foreground mt-1.5 font-medium leading-5">
Self sign for free!
</h1>
</Link>
</motion.div>
))
@@ -59,7 +59,13 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
<SheetContent className="w-full max-w-[400px]">
<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>
<motion.div
@@ -89,7 +95,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
}}
>
<Link
className="text-2xl font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
href={href}
onClick={() => handleMenuItemClick()}
>
@@ -103,7 +109,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Link
href="https://twitter.com/documenso"
target="_blank"
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
className="text-foreground hover:text-foreground/80"
>
<Twitter className="h-6 w-6" />
</Link>
@@ -111,7 +117,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Link
href="https://github.com/documenso/documenso"
target="_blank"
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
className="text-foreground hover:text-foreground/80"
>
<Github className="h-6 w-6" />
</Link>
@@ -119,7 +125,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Link
href="https://documen.so/discord"
target="_blank"
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
className="text-foreground hover:text-foreground/80"
>
<MessagesSquare className="h-6 w-6" />
</Link>
@@ -19,7 +19,7 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
<Image
src={backgroundPattern}
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>
<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">
<Card className="col-span-2" degrees={45} gradient>
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
<p className="col-span-12 leading-relaxed text-[#555E67] lg:col-span-6">
<p className="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
<strong className="block">Open Source or Hosted.</strong>
Its up to you. Either clone our repository or rely on our easy to use hosting
solution.
</p>
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
<Image src={cardOpenFigure} alt="its fast" className="max-w-[80%] lg:max-w-full" />
<Image
src={cardOpenFigure}
alt="its fast"
className="max-w-[80%] dark:contrast-[70%] dark:hue-rotate-180 dark:invert lg:max-w-full"
/>
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<p className="text-foreground/80 leading-relaxed">
<strong className="block">Build on top.</strong>
Make it your own through advanced customization and adjustability.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardBuildFigure} alt="its fast" className="w-full max-w-xs" />
<Image
src={cardBuildFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<p className="text-foreground/80 leading-relaxed">
<strong className="block">Template Store (Soon).</strong>
Choose a template from the community app store. Or submit your own template for others
to use.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardTemplateFigure} alt="its fast" className="w-full max-w-sm" />
<Image
src={cardTemplateFigure}
alt="its fast"
className="w-full max-w-sm dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div>
</CardContent>
</Card>
@@ -41,10 +41,13 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<AnimatePresence>
<motion.button
key="MONTHLY"
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', {
'text-slate-900': period === 'MONTHLY',
'hover:text-slate-900/80': period !== 'MONTHLY',
})}
className={cn(
'text-muted-foreground relative flex items-center gap-x-2.5 px-1 py-2.5',
{
'text-foreground': period === 'MONTHLY',
'hover:text-foreground/80': period !== 'MONTHLY',
},
)}
onClick={() => setPeriod('MONTHLY')}
>
Monthly
@@ -58,14 +61,17 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<motion.button
key="YEARLY"
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', {
'text-slate-900': period === 'YEARLY',
'hover:text-slate-900/80': period !== 'YEARLY',
})}
className={cn(
'text-muted-foreground relative flex items-center gap-x-2.5 px-1 py-2.5',
{
'text-foreground': period === 'YEARLY',
'hover:text-foreground/80': period !== 'YEARLY',
},
)}
onClick={() => setPeriod('YEARLY')}
>
Yearly
<div className="block rounded-full bg-slate-200 px-2 py-0.5 text-xs text-slate-700">
<div className="bg-muted text-foreground block rounded-full px-2 py-0.5 text-xs">
Save $60
</div>
{period === 'YEARLY' && (
@@ -81,12 +87,12 @@ 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
data-plan="self-hosted"
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5"
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">Self Hosted</p>
<p className="text-primary mt-2.5 text-xl font-medium">Free</p>
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
<p className="text-foreground mt-4 max-w-[30ch] text-center">
For small teams and individuals who need a simple solution
</p>
@@ -100,20 +106,20 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</Link>
<div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Host your own instance</p>
<p className="py-4 text-slate-900">Full Control</p>
<p className="py-4 text-slate-900">Customizability</p>
<p className="py-4 text-slate-900">Docker Ready</p>
<p className="py-4 text-slate-900">Community Support</p>
<p className="py-4 text-slate-900">Free, Forever</p>
<p className="text-foreground py-4 font-medium">Host your own instance</p>
<p className="text-foreground py-4">Full Control</p>
<p className="text-foreground py-4">Customizability</p>
<p className="text-foreground py-4">Docker Ready</p>
<p className="text-foreground py-4">Community Support</p>
<p className="text-foreground py-4">Free, Forever</p>
</div>
</div>
<div
data-plan="community"
className="border-primary flex flex-col items-center justify-center rounded-lg border-2 bg-white px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380] shadow-slate-900/5"
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">Community</p>
<div className="text-primary mt-2.5 text-xl font-medium">
<AnimatePresence mode="wait">
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
@@ -121,7 +127,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</AnimatePresence>
</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.
</p>
@@ -130,25 +136,25 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</ClaimPlanDialog>
<div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Documenso Early Adopter Deal:</p>
<p className="py-4 text-slate-900">Join the movement</p>
<p className="py-4 text-slate-900">Simple signing solution</p>
<p className="py-4 text-slate-900">Email and Slack assistance</p>
<p className="py-4 text-slate-900">
<p className="text-foreground py-4 font-medium">Documenso Early Adopter Deal:</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 and Slack assistance</p>
<p className="text-foreground py-4">
<strong>Includes all upcoming features</strong>
</p>
<p className="py-4 text-slate-900">Fixed, straightforward pricing</p>
<p className="text-foreground py-4">Fixed, straightforward pricing</p>
</div>
</div>
<div
data-plan="enterprise"
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5"
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="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.
</p>
@@ -162,12 +168,12 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</Link>
<div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Everything in Community, plus:</p>
<p className="py-4 text-slate-900">Custom Subdomain</p>
<p className="py-4 text-slate-900">Compliance Check</p>
<p className="py-4 text-slate-900">Guaranteed Uptime</p>
<p className="py-4 text-slate-900">Reporting & Analysis</p>
<p className="py-4 text-slate-900">24/7 Support</p>
<p className="text-foreground py-4 font-medium">Everything in Community, plus:</p>
<p className="text-foreground py-4">Custom Subdomain</p>
<p className="text-foreground py-4">Compliance Check</p>
<p className="text-foreground py-4">Guaranteed Uptime</p>
<p className="text-foreground py-4">Reporting & Analysis</p>
<p className="text-foreground py-4">24/7 Support</p>
</div>
</div>
</div>
@@ -23,7 +23,7 @@ export const ShareConnectPaidWidgetBento = ({
<Image
src={backgroundPattern}
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>
<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">
<Card className="col-span-2 lg:col-span-1" degrees={120} gradient>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<p className="text-foreground/80 leading-relaxed">
<strong className="block">Easy Sharing (Soon).</strong>
Receive your personal link to share with everyone you care about.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardSharingFigure} alt="its fast" className="w-full max-w-xs" />
<Image
src={cardSharingFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<p className="text-foreground/80 leading-relaxed">
<strong className="block">Connections (Soon).</strong>
Create connections and automations with Zapier and more to integrate with your
favorite tools.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardConnectionsFigure} alt="its fast" className="w-full max-w-sm" />
<Image
src={cardConnectionsFigure}
alt="its fast"
className="w-full max-w-sm dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<p className="text-foreground/80 leading-relaxed">
<strong className="block">Get paid (Soon).</strong>
Integrated payments with stripe so you dont have to worry about getting paid.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardPaidFigure} alt="its fast" className="w-full max-w-[14rem]" />
<Image
src={cardPaidFigure}
alt="its fast"
className="w-full max-w-[14rem] dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<p className="text-foreground/80 leading-relaxed">
<strong className="block">React Widget (Soon).</strong>
Easily embed Documenso into your product. Simply copy and paste our react widget into
your application.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardWidgetFigure} alt="its fast" className="w-full max-w-xs" />
<Image
src={cardWidgetFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div>
</CardContent>
</Card>
@@ -3,7 +3,6 @@
import { createElement } from 'react';
import { DateTime } from 'luxon';
import { nanoid } from 'nanoid';
import { PDFDocument } from 'pdf-lib';
import { match } from 'ts-pattern';
import { z } from 'zod';
@@ -13,6 +12,8 @@ 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,
@@ -25,7 +26,10 @@ import {
} from '@documenso/prisma/client';
const ZCreateSinglePlayerDocumentSchema = z.object({
document: z.string(),
documentData: z.object({
data: z.string(),
type: z.nativeEnum(DocumentDataType),
}),
documentName: z.string(),
signer: z.object({
email: z.string().email().min(1),
@@ -54,7 +58,13 @@ export type TCreateSinglePlayerDocumentSchema = z.infer<typeof ZCreateSinglePlay
export const createSinglePlayerDocument = async (
value: TCreateSinglePlayerDocumentSchema,
): Promise<string> => {
const { signer, fields, document, documentName } = ZCreateSinglePlayerDocumentSchema.parse(value);
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();
@@ -89,81 +99,87 @@ export const createSinglePlayerDocument = async (
const pdfBytes = await doc.save();
const documentToken = await prisma.$transaction(async (tx) => {
const documentToken = nanoid();
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,
},
});
// 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 documentDataBytes = Buffer.from(pdfBytes).toString('base64');
const { id: documentDataId } = await tx.documentData.create({
data: {
type: DocumentDataType.BYTES_64,
data: documentDataBytes,
initialData: documentDataBytes,
},
});
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 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 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({
// Create fields and signatures.
await Promise.all(
fields.map(async (field) => {
const insertedField = await tx.field.create({
data: {
fieldId: insertedField.id,
signatureImageAsBase64,
typedSignature,
documentId: document.id,
recipientId: recipient.id,
...mapField(field, signer),
},
});
}
}),
);
return documentToken;
});
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,
},
);
// Todo: Handle `downloadLink`
const template = createElement(DocumentSelfSignedEmailTemplate, {
downloadLink: 'https://documenso.com',
downloadLink: `${process.env.NEXT_PUBLIC_MARKETING_URL}/single-player-mode/${documentToken}`,
documentName: documentName,
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
});
@@ -6,6 +6,8 @@ import Link from 'next/link';
import { Share } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { base64 } from '@documenso/lib/universal/base64';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
@@ -25,17 +27,18 @@ interface SinglePlayerModeSuccessProps {
document: DocumentWithRecipient;
}
export default function SinglePlayerModeSuccess({
className,
document,
}: SinglePlayerModeSuccessProps) {
export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerModeSuccessProps) => {
const { getFlag } = useFeatureFlags();
const isConfettiEnabled = getFlag('marketing_spm_confetti');
const [showDocumentDialog, setShowDocumentDialog] = useState(false);
const [isFetchingDocumentFile, setIsFetchingDocumentFile] = useState(false);
const [documentFile, setDocumentFile] = useState<string | null>(null);
const { toast } = useToast();
const handleShowDocumentDialog = async () => {
const onShowDocumentClick = async () => {
if (isFetchingDocumentFile) {
return;
}
@@ -45,7 +48,7 @@ export default function SinglePlayerModeSuccess({
try {
const data = await getFile(document.documentData);
setDocumentFile(Buffer.from(data).toString('base64'));
setDocumentFile(base64.encode(data));
setShowDocumentDialog(true);
} catch {
@@ -66,38 +69,39 @@ export default function SinglePlayerModeSuccess({
return (
<div className="flex min-h-[calc(100vh-10rem)] flex-col items-center justify-center sm:min-h-[calc(100vh-13rem)]">
<ConfettiScreen duration={3000} gravity={0.075} initialVelocityY={50} wind={0.005} />
{isConfettiEnabled && (
<ConfettiScreen duration={3000} gravity={0.075} initialVelocityY={50} wind={0.005} />
)}
<h2 className="text-center text-2xl font-semibold leading-normal md:text-3xl lg:mb-2 lg:text-4xl">
<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>
<h3 className="text-foreground/80 mb-6 text-center text-lg font-semibold md:text-xl lg:mb-8 lg:text-3xl">
{document.title}
</h3>
<SigningCard3D
className="mt-8"
name={document.Recipient.name || document.Recipient.email}
signingCelebrationImage={signingCelebration}
/>
<div className="mt-8 w-full">
<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">
{/* TODO: Hook this up */}
<Button variant="outline" className="flex-1" disabled>
<Button variant="outline" className="flex-1 bg-transparent backdrop-blur-sm" disabled>
<Share className="mr-2 h-5 w-5" />
Share
</Button>
<DocumentDownloadButton
className="flex-1"
className="flex-1 bg-transparent backdrop-blur-sm"
fileName={document.title}
documentData={document.documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
<Button
onClick={async () => handleShowDocumentDialog()}
onClick={async () => onShowDocumentClick()}
loading={isFetchingDocumentFile}
className="col-span-2"
>
@@ -107,16 +111,16 @@ export default function SinglePlayerModeSuccess({
</div>
</div>
<p className="text-muted-foreground/60 mt-36 text-center text-sm">
View the{' '}
<p className="text-muted-foreground/60 mt-16 text-center text-sm">
Create a{' '}
<Link
href="/pricing"
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
target="_blank"
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
>
community plan
free account
</Link>{' '}
to access the full range of features provided by Documenso
to access your signed documents at any time
</p>
<DocumentDialog
@@ -126,4 +130,4 @@ export default function SinglePlayerModeSuccess({
/>
</div>
);
}
};
@@ -181,16 +181,16 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
{...props}
>
<div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8">
<div className="col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed text-[#727272] lg:col-span-7">
<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}
</div>
<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)}
>
<h3 className="text-2xl font-semibold">Sign up for the community 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
</p>
@@ -198,7 +198,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<AnimatePresence>
<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?
</label>
@@ -211,7 +211,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
id="email"
type="email"
placeholder=""
className="w-full bg-white pr-16"
className="bg-background w-full pr-16"
disabled={isSubmitting}
onKeyDown={(e) =>
field.value !== '' &&
@@ -255,7 +255,10 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
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?
</label>
@@ -268,7 +271,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
id="name"
type="text"
placeholder=""
className="w-full bg-white pr-16"
className="bg-background w-full pr-16"
disabled={isSubmitting}
onKeyDown={(e) =>
field.value !== '' &&
@@ -300,11 +303,11 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<div className="mt-12 flex-1" />
<div className="flex items-center justify-between">
<p className="text-xs text-[#AFAFAF]">{stepsRemaining} step(s) until signed</p>
<p className="block text-xs text-[#AFAFAF] md:hidden">Minimise contract</p>
<p className="text-muted-foreground text-xs">{stepsRemaining} step(s) until signed</p>
<p className="text-muted-foreground block text-xs md:hidden">Minimise contract</p>
</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
className={cn('bg-primary/60 absolute inset-y-0 left-0 duration-200', {
'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">
{!signatureText && signatureDataUrl && (
<img src={signatureDataUrl} alt="user signature" className="h-full" />
<img
src={signatureDataUrl}
alt="user signature"
className="h-full dark:invert"
/>
)}
{signatureText && (
<p
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}
@@ -342,7 +349,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
>
<Input
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"
disabled={isSubmitting}
{...register('signatureText', {
@@ -356,7 +363,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<Button
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}
>
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
@@ -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>;
}
+3
View File
@@ -0,0 +1,3 @@
node_modules/
playwright-report/
playwright/.auth
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
import { deleteUserAndItsData } from '@documenso/lib/server-only/user/delete-user-and-data';
async function teardown() {
if (!process.env.E2E_TEST_USERNAME || !process.env.E2E_TEST_AUTHENTICATE_USERNAME) {
return;
}
try {
await deleteUserAndItsData(process.env.E2E_TEST_USERNAME);
await deleteUserAndItsData(process.env.E2E_TEST_AUTHENTICATE_USERNAME);
} catch (e) {
throw new Error(`Error deleting user: ${e}`);
}
}
export default teardown;
+8 -4
View File
@@ -8,6 +8,9 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"test:dev": "playwright test",
"test:e2e": "start-server-and-test \"npm run start\" http://localhost:3000 \"playwright test\"",
"clean": "rimraf .next && rimraf node_modules",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
@@ -21,10 +24,9 @@
"@tanstack/react-query": "^4.29.5",
"formidable": "^2.1.1",
"framer-motion": "^10.12.8",
"lucide-react": "^0.277.0",
"lucide-react": "^0.279.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"nanoid": "^4.0.2",
"next": "13.4.19",
"next-auth": "4.22.3",
"next-plausible": "^3.10.1",
@@ -41,13 +43,15 @@
"sharp": "0.32.5",
"ts-pattern": "^5.0.5",
"typescript": "5.1.6",
"zod": "^3.21.4"
"zod": "^3.21.4",
"start-server-and-test": "^2.0.1"
},
"devDependencies": {
"@playwright/test": "^1.38.0",
"@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1",
"@types/node": "20.1.0",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7"
}
}
}
+66
View File
@@ -0,0 +1,66 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
import { config as dotenvConfig } from 'dotenv';
dotenvConfig();
export const STORAGE_STATE = 'playwright/.auth/user.json';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './src/tests/e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
navigationTimeout: 60 * 1000,
},
/* Configure projects for major browsers */
projects: [
{ name: 'setup', testMatch: '**/*.setup.ts' },
{
name: 'Authenticated User Tests',
testMatch: '*.authenticated.spec.ts',
dependencies: ['setup', 'cleanup db'],
use: {
...devices['Desktop Chrome'],
storageState: STORAGE_STATE,
},
},
{
name: 'Unauthenticated User Tests',
dependencies: ['cleanup db'],
use: {
...devices['Desktop Chrome'],
},
testMatch: '*.unauthenticated.spec.ts',
testIgnore: ['*.setup.ts', '*.authenticated.spec.ts'],
},
{
name: 'cleanup db',
testMatch: /global.teardown\.ts/,
},
],
globalTeardown: './global.teardown.ts',
});
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

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

Before

Width:  |  Height:  |  Size: 629 B

@@ -166,6 +166,7 @@ export const EditDocumentForm = ({
{step === 'signers' && (
<AddSignersFormPartial
key={recipients.length}
documentFlow={documentFlow.signers}
recipients={recipients}
fields={fields}
@@ -176,6 +177,7 @@ export const EditDocumentForm = ({
{step === 'fields' && (
<AddFieldsFormPartial
key={fields.length}
documentFlow={documentFlow.fields}
recipients={recipients}
fields={fields}
@@ -7,7 +7,11 @@ import { useSession } from 'next-auth/react';
import { match } from 'ts-pattern';
import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
export type DataTableActionButtonProps = {
row: Document & {
@@ -18,11 +22,16 @@ export type DataTableActionButtonProps = {
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const { data: session } = useSession();
const { toast } = useToast();
const [, copyToClipboard] = useCopyToClipboard();
if (!session) {
return null;
}
const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } =
trpc.shareLink.createOrGetShareLink.useMutation();
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id;
@@ -32,6 +41,20 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const isComplete = row.status === DocumentStatus.COMPLETED;
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({
isOwner,
isRecipient,
@@ -57,8 +80,8 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
</Button>
))
.otherwise(() => (
<Button className="w-24" disabled>
<Share className="-ml-1 mr-2 h-4 w-4" />
<Button className="w-24" loading={isCreatingShareLink} onClick={onShareClick}>
{!isCreatingShareLink && <Share className="-ml-1 mr-2 h-4 w-4" />}
Share
</Button>
));
@@ -7,6 +7,7 @@ import {
Download,
Edit,
History,
Loader,
MoreHorizontal,
Pencil,
Share,
@@ -18,7 +19,8 @@ import { useSession } from 'next-auth/react';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client';
import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc } from '@documenso/trpc/client';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { trpc as trpcReact } from '@documenso/trpc/react';
import {
DropdownMenu,
DropdownMenuContent,
@@ -26,6 +28,9 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
export type DataTableActionDropdownProps = {
row: Document & {
@@ -36,11 +41,16 @@ export type DataTableActionDropdownProps = {
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
const { data: session } = useSession();
const { toast } = useToast();
const [, copyToClipboard] = useCopyToClipboard();
if (!session) {
return null;
}
const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } =
trpcReact.shareLink.createOrGetShareLink.useMutation();
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id;
@@ -50,15 +60,29 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
const isComplete = row.status === DocumentStatus.COMPLETED;
// 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.',
});
};
const onDownloadClick = async () => {
let document: DocumentWithData | null = null;
if (!recipient) {
document = await trpc.document.getDocumentById.query({
document = await trpcClient.document.getDocumentById.query({
id: row.id,
});
} else {
document = await trpc.document.getDocumentByToken.query({
document = await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
}
@@ -88,7 +112,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
return (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="h-5 w-5 text-gray-500" />
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
@@ -135,8 +159,12 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
Resend
</DropdownMenuItem>
<DropdownMenuItem disabled>
<Share className="mr-2 h-4 w-4" />
<DropdownMenuItem onClick={onShareClick}>
{isCreatingShareLink ? (
<Loader className="mr-2 h-4 w-4" />
) : (
<Share className="mr-2 h-4 w-4" />
)}
Share
</DropdownMenuItem>
</DropdownMenuContent>
@@ -92,8 +92,8 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
</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 className="bg-background/50 absolute inset-0 flex items-center justify-center">
<Loader className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
)}
</div>
@@ -0,0 +1,50 @@
import { Bird, CheckCircle2 } from 'lucide-react';
import { match } from 'ts-pattern';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
export type EmptyDocumentProps = { status: ExtendedDocumentStatus };
export const EmptyDocumentState = ({ status }: EmptyDocumentProps) => {
const {
title,
message,
icon: Icon,
} = match(status)
.with(ExtendedDocumentStatus.COMPLETED, () => ({
title: 'Nothing to do',
message:
'There are no completed documents yet. Documents that you have created or received that become completed will appear here later.',
icon: CheckCircle2,
}))
.with(ExtendedDocumentStatus.DRAFT, () => ({
title: 'No active drafts',
message:
'There are no active drafts at then current moment. You can upload a document to start drafting.',
icon: CheckCircle2,
}))
.with(ExtendedDocumentStatus.ALL, () => ({
title: "We're all empty",
message:
'You have not yet created or received any documents. To create a document please upload one.',
icon: Bird,
}))
.otherwise(() => ({
title: 'Nothing to do',
message:
'All documents are currently actioned. Any new documents are sent or recieved they will start to appear here.',
icon: CheckCircle2,
}));
return (
<div className="text-muted-foreground/60 flex h-60 flex-col items-center justify-center gap-y-4">
<Icon className="h-12 w-12" strokeWidth={1.5} />
<div className="text-center">
<h3 className="text-lg font-semibold">{title}</h3>
<p className="mt-2 max-w-[60ch]">{message}</p>
</div>
</div>
);
};
+32 -28
View File
@@ -12,6 +12,7 @@ import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/ty
import { DocumentStatus } from '~/components/formatter/document-status';
import { DocumentsDataTable } from './data-table';
import { EmptyDocumentState } from './empty-state';
import { UploadDocument } from './upload-document';
export type DocumentsPageProps = {
@@ -62,41 +63,44 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<UploadDocument />
<h1 className="mt-12 text-4xl font-semibold">Documents</h1>
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<h1 className="text-4xl font-semibold">Documents</h1>
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
<Tabs defaultValue={status} className="overflow-x-auto">
<TabsList>
{[
ExtendedDocumentStatus.INBOX,
ExtendedDocumentStatus.PENDING,
ExtendedDocumentStatus.COMPLETED,
ExtendedDocumentStatus.DRAFT,
ExtendedDocumentStatus.ALL,
].map((value) => (
<TabsTrigger key={value} className="min-w-[60px]" value={value} asChild>
<Link href={getTabHref(value)} scroll={false}>
<DocumentStatus status={value} />
<div className="flex flex-wrap gap-x-4 gap-y-6 overflow-hidden">
<Tabs defaultValue={status} className="overflow-x-auto">
<TabsList>
{[
ExtendedDocumentStatus.INBOX,
ExtendedDocumentStatus.PENDING,
ExtendedDocumentStatus.COMPLETED,
ExtendedDocumentStatus.DRAFT,
ExtendedDocumentStatus.ALL,
].map((value) => (
<TabsTrigger key={value} className="min-w-[60px]" value={value} asChild>
<Link href={getTabHref(value)} scroll={false}>
<DocumentStatus status={value} />
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats[value], 99)}
{stats[value] > 99 && '+'}
</span>
)}
</Link>
</TabsTrigger>
))}
</TabsList>
</Tabs>
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats[value], 99)}
{stats[value] > 99 && '+'}
</span>
)}
</Link>
</TabsTrigger>
))}
</TabsList>
</Tabs>
<div className="flex flex-1 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<PeriodSelector />
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<PeriodSelector />
</div>
</div>
</div>
<div className="mt-8">
<DocumentsDataTable results={results} />
{results.count > 0 && <DocumentsDataTable results={results} />}
{results.count === 0 && <EmptyDocumentState status={status} />}
</div>
</div>
);
@@ -67,7 +67,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
<DocumentDropzone className="min-h-[40vh]" onDrop={onFileDrop} />
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<div className="bg-background/50 absolute inset-0 flex items-center justify-center">
<Loader className="text-muted-foreground h-12 w-12 animate-spin" />
</div>
)}
@@ -0,0 +1,153 @@
import { ImageResponse, NextResponse } from 'next/server';
import { P, match } from 'ts-pattern';
import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/share/get-recipient-or-sender-by-share-link-slug';
import { Logo } from '~/components/branding/logo';
import { getAssetBuffer } from '~/helpers/get-asset-buffer';
const CARD_OFFSET_TOP = 152;
const CARD_OFFSET_LEFT = 350;
const CARD_WIDTH = 500;
const CARD_HEIGHT = 250;
const size = {
width: 1200,
height: 630,
};
type SharePageOpenGraphImageProps = {
params: { slug: string };
};
export async function GET(_request: Request, { params: { slug } }: SharePageOpenGraphImageProps) {
const [interSemiBold, interRegular, caveatRegular, shareFrameImage] = await Promise.all([
getAssetBuffer('/fonts/inter-semibold.ttf'),
getAssetBuffer('/fonts/inter-regular.ttf'),
getAssetBuffer('/fonts/caveat-regular.ttf'),
getAssetBuffer('/static/og-share-frame.png'),
]);
const recipientOrSender = await getRecipientOrSenderByShareLinkSlug({ slug }).catch(() => null);
if (!recipientOrSender) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
const isRecipient = 'Signature' in recipientOrSender;
const signatureImage = match(recipientOrSender)
.with({ Signature: P.array(P._) }, (recipient) => {
return recipient.Signature?.[0]?.signatureImageAsBase64 || null;
})
.otherwise((sender) => {
return sender.signature || null;
});
const signatureName = match(recipientOrSender)
.with({ Signature: P.array(P._) }, (recipient) => {
return recipient.name || recipient.email;
})
.otherwise((sender) => {
return sender.name || sender.email;
});
return new ImageResponse(
(
<div tw="relative flex h-full w-full">
{/* @ts-expect-error Lack of typing from ImageResponse */}
<img src={shareFrameImage} alt="og-share-frame" tw="absolute inset-0 w-full h-full" />
<div tw="absolute top-20 flex w-full items-center justify-center">
{/* @ts-expect-error Lack of typing from ImageResponse */}
<Logo tw="h-8 w-60" />
</div>
{signatureImage ? (
<div
tw="absolute py-6 px-12 flex items-center justify-center text-center"
style={{
top: `${CARD_OFFSET_TOP}px`,
left: `${CARD_OFFSET_LEFT}px`,
width: `${CARD_WIDTH}px`,
height: `${CARD_HEIGHT}px`,
}}
>
<img src={signatureImage} alt="signature" tw="opacity-60 h-full max-w-[100%]" />
</div>
) : (
<p
tw="absolute py-6 px-12 -mt-2 flex items-center justify-center text-center text-slate-500"
style={{
fontFamily: 'Caveat',
fontSize: `${Math.max(
Math.min((CARD_WIDTH * 1.5) / signatureName.length, 80),
36,
)}px`,
top: `${CARD_OFFSET_TOP}px`,
left: `${CARD_OFFSET_LEFT}px`,
width: `${CARD_WIDTH}px`,
height: `${CARD_HEIGHT}px`,
}}
>
{signatureName}
</p>
)}
{/* <div
tw="absolute flex items-center justify-center text-slate-500"
style={{
top: `${CARD_OFFSET_TOP + CARD_HEIGHT - 45}px`,
left: `${CARD_OFFSET_LEFT}`,
width: `${CARD_WIDTH}px`,
fontSize: '30px',
}}
>
{signatureName}
</div> */}
<div
tw="absolute flex flex-col items-center justify-center pt-12 w-full"
style={{
top: `${CARD_OFFSET_TOP + CARD_HEIGHT}px`,
}}
>
<h2
tw="text-3xl text-slate-500"
style={{
fontFamily: 'Inter',
fontWeight: 600,
}}
>
{isRecipient
? 'I just signed with Documenso and you can too!'
: 'I just sent a document with Documenso and you can too!'}
</h2>
</div>
</div>
),
{
...size,
fonts: [
{
name: 'Caveat',
data: caveatRegular,
style: 'italic',
},
{
name: 'Inter',
data: interRegular,
style: 'normal',
weight: 400,
},
{
name: 'Inter',
data: interSemiBold,
style: 'normal',
weight: 600,
},
],
},
);
}
@@ -0,0 +1,39 @@
import { Metadata } from 'next';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { APP_BASE_URL } from '@documenso/lib/constants/app';
type SharePageProps = {
params: { slug: string };
};
export function generateMetadata({ params: { slug } }: SharePageProps) {
return {
title: 'Documenso - Share',
description: 'I just signed a document with Documenso!',
openGraph: {
title: 'Documenso - Join the open source signing revolution',
description: 'I just signed with Documenso!',
type: 'website',
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
description: 'I just signed with Documenso!',
},
} satisfies Metadata;
}
export default function SharePage() {
const userAgent = headers().get('User-Agent') ?? '';
// https://stackoverflow.com/questions/47026171/how-to-detect-bots-for-open-graph-with-user-agent
if (/bot|facebookexternalhit|WhatsApp|google|bing|duckduckbot|MetaInspector/i.test(userAgent)) {
return null;
}
redirect(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001');
}
@@ -1,7 +1,7 @@
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { CheckCircle2, Clock8, Share } from 'lucide-react';
import { CheckCircle2, Clock8 } from 'lucide-react';
import { match } from 'ts-pattern';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
@@ -9,11 +9,12 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { SigningCard } from '@documenso/ui/components/signing-card';
import { Button } from '@documenso/ui/primitives/button';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
import signingCelebration from '~/assets/signing-celebration.png';
import { ShareButton } from './share-button';
export type CompletedSigningPageProps = {
params: {
token?: string;
@@ -52,11 +53,11 @@ export default async function CompletedSigningPage({
recipient.email;
return (
<div className="flex flex-col items-center pt-24">
<div className="flex flex-col items-center pt-24 lg:pt-36 xl:pt-44">
{/* Card with recipient */}
<SigningCard name={recipientName} signingCelebrationImage={signingCelebration} />
<SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />
<div className="mt-6">
<div className="relative mt-6 flex w-full flex-col items-center">
{match(document.status)
.with(DocumentStatus.COMPLETED, () => (
<div className="text-documenso-700 flex items-center text-center">
@@ -70,45 +71,44 @@ export default async function CompletedSigningPage({
<span className="text-sm">Waiting for others to sign</span>
</div>
))}
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
You have signed "{document.title}"
</h2>
{match(document.status)
.with(DocumentStatus.COMPLETED, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
Everyone has signed! You will receive an Email copy of the signed document.
</p>
))
.otherwise(() => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
You will receive an Email copy of the signed document once everyone has signed.
</p>
))}
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
<ShareButton documentId={document.id} token={recipient.token} />
<DocumentDownloadButton
className="flex-1"
fileName={document.title}
documentData={documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
</div>
<p className="text-muted-foreground/60 mt-36 text-sm">
Want to send slick signing links like this one?{' '}
<Link
href="https://documenso.com"
className="text-documenso-700 hover:text-documenso-600"
>
Check out Documenso.
</Link>
</p>
</div>
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
You have signed "{document.title}"
</h2>
{match(document.status)
.with(DocumentStatus.COMPLETED, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
Everyone has signed! You will receive an Email copy of the signed document.
</p>
))
.otherwise(() => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
You will receive an Email copy of the signed document once everyone has signed.
</p>
))}
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
{/* TODO: Hook this up */}
<Button variant="outline" className="flex-1">
<Share className="mr-2 h-5 w-5" />
Share
</Button>
<DocumentDownloadButton
className="flex-1"
fileName={document.title}
documentData={documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
</div>
<p className="text-muted-foreground/60 mt-36 text-sm">
Want to send slick signing links like this one?{' '}
<Link href="https://documenso.com" className="text-documenso-700 hover:text-documenso-600">
Check out Documenso.
</Link>
</p>
</div>
);
}
@@ -0,0 +1,146 @@
'use client';
import { HTMLAttributes, useState } from 'react';
import { Copy, Share, Twitter } from 'lucide-react';
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
export type ShareButtonProps = HTMLAttributes<HTMLButtonElement> & {
token: string;
documentId: number;
};
export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
const { toast } = useToast();
const [, copyToClipboard] = useCopyToClipboard();
const [isOpen, setIsOpen] = useState(false);
const {
mutateAsync: createOrGetShareLink,
data: shareLink,
isLoading,
} = trpc.shareLink.createOrGetShareLink.useMutation();
const onOpenChange = (nextOpen: boolean) => {
if (nextOpen) {
void createOrGetShareLink({
token,
documentId,
});
}
setIsOpen(nextOpen);
};
const onCopyClick = async () => {
let { slug = '' } = shareLink || {};
if (!slug) {
const result = await createOrGetShareLink({
token,
documentId,
});
slug = result.slug;
}
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
toast({
title: 'Copied to clipboard',
description: 'The sharing link has been copied to your clipboard.',
});
setIsOpen(false);
};
const onTweetClick = async () => {
let { slug = '' } = shareLink || {};
if (!slug) {
const result = await createOrGetShareLink({
token,
documentId,
});
slug = result.slug;
}
window.open(
generateTwitterIntent(
`I just ${token ? 'signed' : 'sent'} a document with @documenso. Check it out!`,
`${window.location.origin}/share/${slug}`,
),
'_blank',
);
setIsOpen(false);
};
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<Button
variant="outline"
disabled={!token || !documentId}
className="flex-1"
loading={isLoading}
>
{!isLoading && <Share className="mr-2 h-5 w-5" />}
Share
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Share</DialogTitle>
<DialogDescription className="mt-4">Share your signing experience!</DialogDescription>
</DialogHeader>
<div className="flex w-full flex-col">
<div className="rounded-md border p-4">
I just {token ? 'signed' : 'sent'} a document with{' '}
<span className="font-medium text-blue-400">@documenso</span>
. Check it out!
<span className="mt-2 block" />
<span className="font-medium text-blue-400">
{window.location.origin}/share/{shareLink?.slug || '...'}
</span>
</div>
<Button variant="outline" className="mt-4" onClick={onTweetClick}>
<Twitter className="mr-2 h-4 w-4" />
Tweet
</Button>
<div className="relative flex items-center justify-center gap-x-4 py-4 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or</span>
<div className="bg-border h-px flex-1" />
</div>
<Button variant="outline" onClick={onCopyClick}>
<Copy className="mr-2 h-4 w-4" />
Copy Link
</Button>
</div>
</DialogContent>
</Dialog>
);
};
@@ -77,7 +77,7 @@ export const DateField = ({ field, recipient }: DateFieldProps) => {
return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center">
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
@@ -81,7 +81,7 @@ export const EmailField = ({ field, recipient }: EmailFieldProps) => {
return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center">
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
@@ -1,12 +1,15 @@
'use client';
import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { Document, Field, Recipient } from '@documenso/prisma/client';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@@ -27,15 +30,22 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
const {
handleSubmit,
formState: { isSubmitting },
} = useForm();
const isComplete = fields.every((f) => f.inserted);
const uninsertedFields = useMemo(() => {
return sortFieldsByPosition(fields.filter((field) => !field.inserted));
}, [fields]);
const onFormSubmit = async () => {
if (!isComplete) {
setValidateUninsertedFields(true);
const isFieldsValid = validateFieldsInserted(fields);
if (!isFieldsValid) {
return;
}
@@ -54,7 +64,16 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
)}
onSubmit={handleSubmit(onFormSubmit)}
>
<div className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}>
{validateUninsertedFields && uninsertedFields[0] && (
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
Click to insert field
</FieldToolTip>
)}
<fieldset
disabled={isSubmitting}
className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}
>
<div className={cn('flex flex-1 flex-col')}>
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
@@ -101,23 +120,18 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
onClick={() => router.back()}
>
Cancel
</Button>
<Button
className="w-full"
type="submit"
size="lg"
disabled={!isComplete || isSubmitting}
>
{isSubmitting && <Loader className="mr-2 h-5 w-5 animate-spin" />}
<Button className="w-full" type="submit" size="lg" loading={isSubmitting}>
Complete
</Button>
</div>
</div>
</div>
</div>
</fieldset>
</form>
);
};
@@ -100,7 +100,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => {
return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center">
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
@@ -1,4 +1,4 @@
import { notFound } from 'next/navigation';
import { notFound, redirect } from 'next/navigation';
import { match } from 'ts-pattern';
@@ -9,7 +9,7 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { FieldType } from '@documenso/prisma/client';
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
@@ -38,7 +38,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
}).catch(() => null),
getFieldsForToken({ token }),
getRecipientByToken({ token }).catch(() => null),
viewedDocument({ token }),
viewedDocument({ token }).catch(() => null),
]);
if (!document || !document.documentData || !recipient) {
@@ -53,6 +53,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
const user = await getServerComponentSession();
if (
document.status === DocumentStatus.COMPLETED ||
recipient.signingStatus === SigningStatus.SIGNED
) {
redirect(`/sign/${token}/complete`);
}
return (
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
@@ -115,7 +115,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center">
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
@@ -130,7 +130,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
<img
src={signature.signatureImageAsBase64}
alt={`Signature for ${recipient.name}`}
className="h-full w-full object-contain"
className="h-full w-full object-contain dark:invert"
/>
)}
@@ -2,10 +2,8 @@
import React from 'react';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { FieldRootContainer } from '@documenso/ui/components/field/field';
export type SignatureFieldProps = {
field: FieldWithSignature;
@@ -22,8 +20,6 @@ export const SigningFieldContainer = ({
onRemove,
children,
}: SignatureFieldProps) => {
const coords = useFieldPageCoords(field);
const onSignFieldClick = async () => {
if (field.inserted) {
return;
@@ -41,40 +37,25 @@ export const SigningFieldContainer = ({
};
return (
<div
className="absolute"
style={{
top: `${coords.y}px`,
left: `${coords.x}px`,
height: `${coords.height}px`,
width: `${coords.width}px`,
}}
>
<Card
className="bg-background relative h-full w-full"
data-inserted={field.inserted ? 'true' : 'false'}
>
<CardContent
className={cn(
'text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2',
)}
<FieldRootContainer field={field}>
{!field.inserted && !loading && (
<button
type="submit"
className="absolute inset-0 z-10 h-full w-full"
onClick={onSignFieldClick}
/>
)}
{field.inserted && !loading && (
<button
className="text-destructive bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 backdrop-blur-sm duration-200 group-hover:opacity-100"
onClick={onRemoveSignedFieldClick}
>
{!field.inserted && !loading && (
<button type="submit" className="absolute inset-0 z-10" onClick={onSignFieldClick} />
)}
Remove
</button>
)}
{field.inserted && !loading && (
<button
className="text-destructive bg-background/40 absolute inset-0 z-10 flex items-center justify-center rounded-md text-sm opacity-0 backdrop-blur-sm duration-200 group-hover:opacity-100"
onClick={onRemoveSignedFieldClick}
>
Remove
</button>
)}
{children}
</CardContent>
</Card>
</div>
{children}
</FieldRootContainer>
);
};
@@ -16,7 +16,7 @@ export default function UnauthenticatedLayout({ children }: UnauthenticatedLayou
<Image
src={backgroundPattern}
alt="background pattern"
className="dark:brightness-95 dark:invert dark:sepia"
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
/>
</div>
+1
View File
@@ -76,6 +76,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</TooltipProvider>
</ThemeProvider>
</PlausibleProvider>
<Toaster />
</FeatureFlagProvider>
</LocaleProvider>
+26
View File
@@ -0,0 +1,26 @@
import Link from 'next/link';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { Button } from '@documenso/ui/primitives/button';
import NotFoundPartial from '~/components/partials/not-found';
export default async function NotFound() {
const session = await getServerComponentSession();
return (
<NotFoundPartial>
{session && (
<Button className="w-32" asChild>
<Link href="/documents">Documents</Link>
</Button>
)}
{!session && (
<Button className="w-32" asChild>
<Link href="/signin">Sign In</Link>
</Button>
)}
</NotFoundPartial>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

@@ -40,7 +40,7 @@ export const StackAvatarsWithTooltip = ({
return (
<TooltipProvider>
<Tooltip>
<Tooltip delayDuration={200}>
<TooltipTrigger className="flex cursor-pointer">
{children || <StackAvatars recipients={recipients} />}
</TooltipTrigger>
@@ -58,7 +58,7 @@ export const StackAvatarsWithTooltip = ({
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
<span className="text-sm text-gray-500">{recipient.email}</span>
<span className="text-muted-foreground text-sm">{recipient.email}</span>
</div>
))}
</div>
@@ -75,7 +75,7 @@ export const StackAvatarsWithTooltip = ({
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
<span className="text-sm text-gray-500">{recipient.email}</span>
<span className="text-muted-foreground text-sm">{recipient.email}</span>
</div>
))}
</div>
@@ -92,7 +92,7 @@ export const StackAvatarsWithTooltip = ({
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
<span className="text-sm text-gray-500">{recipient.email}</span>
<span className="text-muted-foreground text-sm">{recipient.email}</span>
</div>
))}
</div>
@@ -109,7 +109,7 @@ export const StackAvatarsWithTooltip = ({
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
<span className="text-sm text-gray-500">{recipient.email}</span>
<span className="text-muted-foreground text-sm">{recipient.email}</span>
</div>
))}
</div>
@@ -10,6 +10,7 @@ import {
User as LucideUser,
Monitor,
Moon,
Palette,
Sun,
UserCog,
} from 'lucide-react';
@@ -27,7 +28,13 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
@@ -36,8 +43,8 @@ export type ProfileDropdownProps = {
};
export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
const { theme, setTheme } = useTheme();
const { getFlag } = useFeatureFlags();
const { theme, setTheme } = useTheme();
const isUserAdmin = isAdmin(user);
const isBillingEnabled = getFlag('app_billing');
@@ -97,28 +104,30 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
<DropdownMenuSeparator />
{theme === 'light' ? null : (
<DropdownMenuItem onClick={() => setTheme('light')}>
<Sun className="mr-2 h-4 w-4" />
Light Mode
</DropdownMenuItem>
)}
{theme === 'dark' ? null : (
<DropdownMenuItem onClick={() => setTheme('dark')}>
<Moon className="mr-2 h-4 w-4" />
Dark Mode
</DropdownMenuItem>
)}
{theme === 'system' ? null : (
<DropdownMenuItem onClick={() => setTheme('system')}>
<Monitor className="mr-2 h-4 w-4" />
System Theme
</DropdownMenuItem>
)}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Palette className="mr-2 h-4 w-4" />
Themes
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
<DropdownMenuRadioItem value="light">
<Sun className="mr-2 h-4 w-4" /> Light
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">
<Moon className="mr-2 h-4 w-4" />
Dark
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">
<Monitor className="mr-2 h-4 w-4" />
System
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="https://github.com/documenso/documenso" className="cursor-pointer">
<Github className="mr-2 h-4 w-4" />
@@ -17,17 +17,17 @@ const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus> = {
PENDING: {
label: 'Pending',
icon: Clock,
color: 'text-blue-600',
color: 'text-blue-600 dark:text-blue-300',
},
COMPLETED: {
label: 'Completed',
icon: CheckCircle2,
color: 'text-green-500',
color: 'text-green-500 dark:text-green-300',
},
DRAFT: {
label: 'Draft',
icon: File,
color: 'text-yellow-500',
color: 'text-yellow-500 dark:text-yellow-200',
},
INBOX: {
label: 'Inbox',
@@ -1,6 +1,7 @@
'use server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
@@ -8,12 +9,20 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
documentId: number;
};
export const completeDocument = async ({ documentId }: CompleteDocumentActionInput) => {
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
'use server';
const { id: userId } = await getRequiredServerComponentSession();
await sendDocument({
if (email.message || email.subject) {
await upsertDocumentMeta({
documentId,
subject: email.subject,
message: email.message,
});
}
return await sendDocument({
userId,
documentId,
});
+18 -29
View File
@@ -1,8 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Eye, EyeOff, Loader } from 'lucide-react';
@@ -40,8 +38,6 @@ export type SignInFormProps = {
};
export const SignInForm = ({ className }: SignInFormProps) => {
const searchParams = useSearchParams();
const { toast } = useToast();
const [showPassword, setShowPassword] = useState(false);
@@ -57,36 +53,29 @@ export const SignInForm = ({ className }: SignInFormProps) => {
resolver: zodResolver(ZSignInFormSchema),
});
const errorCode = searchParams?.get('error');
useEffect(() => {
let timeout: NodeJS.Timeout | null = null;
if (isErrorCode(errorCode)) {
timeout = setTimeout(() => {
toast({
variant: 'destructive',
description: ERROR_MESSAGES[errorCode] ?? 'An unknown error occurred',
});
}, 0);
}
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
}, [errorCode, toast]);
const onFormSubmit = async ({ email, password }: TSignInFormSchema) => {
try {
await signIn('credentials', {
const result = await signIn('credentials', {
email,
password,
callbackUrl: LOGIN_REDIRECT_PATH,
}).catch((err) => {
console.error(err);
redirect: false,
});
if (result?.error && isErrorCode(result.error)) {
toast({
variant: 'destructive',
description: ERROR_MESSAGES[result.error],
});
return;
}
if (!result?.url) {
throw new Error('An unknown error occurred');
}
window.location.href = result.url;
} catch (err) {
toast({
title: 'An unknown error occurred',
-7
View File
@@ -1,7 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export * from 'framer-motion';
export const MotionDiv = motion.div;
@@ -0,0 +1,66 @@
'use client';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { ChevronLeft } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import backgroundPattern from '~/assets/background-pattern.png';
export type NotFoundPartialProps = {
children?: React.ReactNode;
};
export default function NotFoundPartial({ children }: NotFoundPartialProps) {
const router = useRouter();
return (
<div className={cn('relative max-w-[100vw] overflow-hidden')}>
<div className="absolute -inset-24 -z-10">
<motion.div
className="flex h-full w-full items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 0.8, transition: { duration: 0.5, delay: 0.5 } }}
>
<Image
src={backgroundPattern}
alt="background pattern"
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
/>
</motion.div>
</div>
<div className="container mx-auto flex h-full min-h-screen items-center px-6 py-32">
<div>
<p className="text-muted-foreground font-semibold">404 Page not found</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
<p className="text-muted-foreground mt-4 text-sm">
The page you are looking for was moved, removed, renamed or might never have existed.
</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button
variant="ghost"
className="w-32"
onClick={() => {
void router.back();
}}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Go Back
</Button>
{children}
</div>
</div>
</div>
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
/**
* getAssetBuffer is used to retrieve array buffers for various assets
* that are hosted in the `public` folder.
*
* This exists due to a breakage with `import.meta.url` imports and open graph images,
* once we can identify a fix for this, we can remove this helper.
*
* @param path The path to the asset, relative to the `public` folder.
*/
export const getAssetBuffer = async (path: string) => {
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
return fetch(new URL(path, baseUrl)).then(async (res) => res.arrayBuffer());
};
+35
View File
@@ -0,0 +1,35 @@
import { type Page, expect, test as setup } from '@playwright/test';
import { STORAGE_STATE } from '../../../playwright.config';
const username = process.env.E2E_TEST_AUTHENTICATE_USERNAME || '';
const email = process.env.E2E_TEST_AUTHENTICATE_USER_EMAIL || '';
const password = process.env.E2E_TEST_AUTHENTICATE_USER_PASSWORD || '';
setup('authenticate', async ({ page }: { page: Page }) => {
await page.goto('/signup');
await page.getByLabel('Name').fill(username);
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill(password);
const canvas = page.locator('canvas');
const box = await canvas.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 8, box.y + box.height / 6);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 8, box.y + box.height / 6);
await page.mouse.up();
}
await page.getByRole('button', { name: 'Sign Up' }).click();
await page.goto('/');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill(password);
await page.getByRole('button', { name: 'Sign In' }).click();
await page.waitForURL('/documents');
await expect(page).toHaveURL('/documents');
await page.context().storageState({ path: STORAGE_STATE });
});
+57
View File
@@ -0,0 +1,57 @@
%PDF-1.4
%Óëéá
1 0 obj
<</Title (documenso)
/Producer (Skia/PDF m118 Google Docs Renderer)>>
endobj
3 0 obj
<</ca 1
/BM /Normal>>
endobj
4 0 obj
<</Length 84>> stream
1 0 0 -1 0 842 cm
q
.75 0 0 .75 0 0 cm
1 1 1 RG 1 1 1 rg
/G3 gs
0 0 794 1123 re
f
Q
endstream
endobj
2 0 obj
<</Type /Page
/Resources <</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
/ExtGState <</G3 3 0 R>>>>
/MediaBox [0 0 596 842]
/Contents 4 0 R
/StructParents 0
/Parent 5 0 R>>
endobj
5 0 obj
<</Type /Pages
/Count 1
/Kids [2 0 R]>>
endobj
6 0 obj
<</Type /Catalog
/Pages 5 0 R>>
endobj
xref
0 7
0000000000 65535 f
0000000015 00000 n
0000000269 00000 n
0000000100 00000 n
0000000137 00000 n
0000000457 00000 n
0000000512 00000 n
trailer
<</Size 7
/Root 6 0 R
/Info 1 0 R>>
startxref
559
%%EOF
@@ -0,0 +1,45 @@
import { type Page, expect, test } from '@playwright/test';
test.use({ storageState: { cookies: [], origins: [] } });
/*
Using them sequentially so the 2nd test
uses the details from the 1st (registration) test
*/
test.describe.configure({ mode: 'serial' });
const username = process.env.E2E_TEST_USERNAME || '';
const email = process.env.E2E_TEST_USER_EMAIL || '';
const password = process.env.E2E_TEST_USER_PASSWORD || '';
test('user can sign up with email and password', async ({ page }: { page: Page }) => {
await page.goto('/signup');
await page.getByLabel('Name').fill(username);
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill(password);
const canvas = page.locator('canvas');
const box = await canvas.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
await page.mouse.up();
}
await page.getByRole('button', { name: 'Sign Up' }).click();
await page.waitForURL('/documents');
await expect(page).toHaveURL('/documents');
});
test('user can login with user and password', async ({ page }: { page: Page }) => {
await page.goto('/signin');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill(password);
await page.getByRole('button', { name: 'Sign In' }).click();
await page.waitForURL('/documents');
await expect(page).toHaveURL('/documents');
});
@@ -0,0 +1,38 @@
import path from 'path';
import { expect, test } from '../test-fixtures/documents-page/documents-page';
const signer_name = process.env.E2E_TEST_SIGNER_NAME;
const signer_email = process.env.E2E_TEST_SIGNER_EMAIL;
const signing_subject = process.env.E2E_TEST_SIGNING_SUBJECT;
const signing_message = process.env.E2E_TEST_SIGNING_MESSAGE;
if (!signer_name || !signer_email || !signing_subject || !signing_message) {
throw new Error('Required environment variables for tests are not defined');
}
test.describe('Document upload test', () => {
test.beforeEach(async ({ documentsPage }) => {
await documentsPage.uploadDocument(path.join(__dirname, './documenso.pdf'));
});
test('user can see /documents page', async ({ page, documentsPage }) => {
await documentsPage.goToDocumentsPage();
await expect(page).toHaveTitle('Documenso - The Open Source DocuSign Alternative');
});
test('user can upload a document succesfully', async ({ page }) => {
await expect(page.getByText('Drag & drop your document here.')).toBeVisible();
});
test('user can send the document for signing succesfully', async ({ documentsPage, page }) => {
await documentsPage.addSigner(signer_email, signer_name);
await documentsPage.addSignatureField(signer_name);
await documentsPage.addSubjectAndMessage(signing_subject, signing_message);
await expect(page).toHaveURL('/documents');
await expect(page.getByRole('status').locator('div').nth(2)).toHaveText(
'Your document has been sent successfully.',
);
});
});
@@ -0,0 +1,49 @@
import type { Locator, Page } from '@playwright/test';
export class DocumentsPage {
private readonly fileInput: Locator;
private readonly subject: Locator;
private readonly message: Locator;
// eslint-disable-next-line no-unused-vars
constructor(public readonly page: Page) {
this.fileInput = this.page.locator('input[type=file]');
this.subject = this.page
.locator('div')
.filter({ hasText: /^Subject \(Optional\)$/ })
.locator('input');
this.message = this.page
.locator('div')
.filter({ hasText: /^Message \(Optional\)$/ })
.locator('textarea');
}
async goToDocumentsPage() {
await this.page.goto('/documents');
}
async uploadDocument(filePath: string) {
await this.goToDocumentsPage();
await this.fileInput.setInputFiles(filePath);
}
async addSigner(email: string, name: string) {
await this.page.getByLabel('Email*').fill(email);
await this.page.getByLabel('Name').fill(name);
await this.page.getByRole('button', { name: 'Continue' }).click();
}
async addSignatureField(name: string) {
await this.page
.getByRole('button', { name: `${name} Signature` })
.dragTo(this.page.locator('canvas'));
await this.page.getByRole('button', { name: 'Continue' }).click();
}
async addSubjectAndMessage(subject: string, message: string) {
await this.subject.fill(subject);
await this.message.fill(message);
await this.page.getByRole('button', { name: 'Send' }).click();
}
}
@@ -0,0 +1,13 @@
import { test as base } from '@playwright/test';
import { DocumentsPage } from './DocumentsPageObject';
export const test = base.extend<{ documentsPage: DocumentsPage }>({
documentsPage: async ({ page }, use) => {
const documentsPage = new DocumentsPage(page);
await use(documentsPage);
},
});
export { expect } from '@playwright/test';
+685 -15
View File
@@ -22,6 +22,7 @@
"husky": "^8.0.0",
"lint-staged": "^14.0.0",
"prettier": "^2.5.1",
"rimraf": "^5.0.1",
"turbo": "^1.9.3"
},
"engines": {
@@ -41,7 +42,7 @@
"@hookform/resolvers": "^3.1.0",
"contentlayer": "^0.3.4",
"framer-motion": "^10.12.8",
"lucide-react": "^0.277.0",
"lucide-react": "^0.279.0",
"micro": "^10.0.1",
"next": "13.4.19",
"next-auth": "4.22.3",
@@ -80,10 +81,9 @@
"@tanstack/react-query": "^4.29.5",
"formidable": "^2.1.1",
"framer-motion": "^10.12.8",
"lucide-react": "^0.277.0",
"lucide-react": "^0.279.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"nanoid": "^4.0.2",
"next": "13.4.19",
"next-auth": "4.22.3",
"next-plausible": "^3.10.1",
@@ -98,11 +98,13 @@
"react-icons": "^4.8.0",
"react-rnd": "^10.4.1",
"sharp": "0.32.5",
"start-server-and-test": "^2.0.1",
"ts-pattern": "^5.0.5",
"typescript": "5.1.6",
"zod": "^3.21.4"
},
"devDependencies": {
"@playwright/test": "^1.38.0",
"@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1",
"@types/node": "20.1.0",
@@ -1876,6 +1878,10 @@
"resolved": "packages/prisma",
"link": true
},
"node_modules/@documenso/signing": {
"resolved": "packages/signing",
"link": true
},
"node_modules/@documenso/tailwind-config": {
"resolved": "packages/tailwind-config",
"link": true
@@ -2446,6 +2452,19 @@
"node": ">=6"
}
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@hookform/resolvers": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.0.tgz",
@@ -2531,6 +2550,96 @@
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@@ -2749,6 +2858,20 @@
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@mdx-js/esbuild": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@mdx-js/esbuild/-/esbuild-2.3.0.tgz",
@@ -3649,6 +3772,16 @@
"pako": "^1.0.10"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@pkgr/utils": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz",
@@ -3668,6 +3801,21 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@playwright/test": {
"version": "1.38.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
"integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
"dev": true,
"dependencies": {
"playwright": "1.38.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@prisma/client": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz",
@@ -5317,6 +5465,24 @@
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"node_modules/@sindresorhus/slugify": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz",
@@ -6309,6 +6475,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz",
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A=="
},
"node_modules/@types/node-forge": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.4.tgz",
"integrity": "sha512-08scBQriFsBbm/CuBWOXRMD1RG7ydFW01EDR6vGX27nxcj6E/jGSCOLdICNd8ETwQlLFXVBVA854RX6Y7vPSrQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/nodemailer": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
@@ -7492,6 +7667,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/check-more-types": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
"integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -8997,6 +9180,11 @@
"node": ">=12"
}
},
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
},
"node_modules/duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@@ -10201,6 +10389,20 @@
"node": ">=0.10.0"
}
},
"node_modules/event-stream": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
"integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==",
"dependencies": {
"duplexer": "~0.1.1",
"from": "~0",
"map-stream": "~0.1.0",
"pause-stream": "0.0.11",
"split": "0.3",
"stream-combiner": "~0.0.4",
"through": "~2.3.1"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -10445,6 +10647,20 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/flat-cache/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/flatted": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
@@ -10477,6 +10693,34 @@
"is-callable": "^1.1.3"
}
},
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/foreground-child/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@@ -10558,6 +10802,11 @@
}
}
},
"node_modules/from": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
"integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g=="
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -11960,6 +12209,24 @@
"ws": "*"
}
},
"node_modules/jackspeak": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz",
"integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
@@ -11978,6 +12245,18 @@
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
"integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="
},
"node_modules/joi": {
"version": "17.10.2",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.10.2.tgz",
"integrity": "sha512-hcVhjBxRNW/is3nNLdGLIjkgXetkeGc2wyhydhz8KumG23Aerk4HPjU5zaPAMRqXQFc0xNqXTC7+zQjxr0GlKA==",
"dependencies": {
"@hapi/hoek": "^9.0.0",
"@hapi/topo": "^5.0.0",
"@sideway/address": "^4.1.3",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/jose": {
"version": "4.14.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
@@ -12212,6 +12491,14 @@
"language-subtag-registry": "~0.3.2"
}
},
"node_modules/lazy-ass": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
"integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
"engines": {
"node": "> 0.8"
}
},
"node_modules/leac": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
@@ -12688,9 +12975,9 @@
}
},
"node_modules/lucide-react": {
"version": "0.277.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.277.0.tgz",
"integrity": "sha512-9epmznme+vW14V9d2rsMeLr3fMnf59lYDUOVUg6s7oVN22Zq8h4B30+3CIdFFV9UXCjPG5ZNKHfO/hf96cl46A==",
"version": "0.279.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.279.0.tgz",
"integrity": "sha512-LJ8g66+Bxc3t3x9vKTeK3wn3xucrOQGfJ9ou9GsBwCt2offsrT2BB90XrTrIzE1noYYDe2O8jZaRHi6sAHXNxw==",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
@@ -12759,6 +13046,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g=="
},
"node_modules/markdown-extensions": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz",
@@ -14196,11 +14488,30 @@
}
}
},
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
"engines": {
"node": ">= 6.13.0"
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
},
"node_modules/node-signpdf": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-signpdf/-/node-signpdf-2.0.0.tgz",
"integrity": "sha512-B6fDvD8z2v0pRntQjgPO2ARqxh0pfNwfSn6YEbP7cv6xmPgcphFwcrMA3N47LztmpVbAM3vUFUslX32L6NRYDg==",
"engines": {
"node": ">=12"
},
"peerDependencies": {
"node-forge": "^1.2.1"
}
},
"node_modules/node-sql-parser": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.9.0.tgz",
@@ -14752,6 +15063,31 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-scurry": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
"dev": true,
"dependencies": {
"lru-cache": "^9.1.1 || ^10.0.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
"integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
"dev": true,
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -14768,6 +15104,14 @@
"node": ">=8"
}
},
"node_modules/pause-stream": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
"integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
"dependencies": {
"through": "~2.3"
}
},
"node_modules/pdf-lib": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
@@ -14866,6 +15210,36 @@
"node": ">= 6"
}
},
"node_modules/playwright": {
"version": "1.38.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
"integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
"dev": true,
"dependencies": {
"playwright-core": "1.38.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.38.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
"integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/postcss": {
"version": "8.4.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
@@ -15416,6 +15790,20 @@
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
},
"node_modules/ps-tree": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz",
"integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==",
"dependencies": {
"event-stream": "=3.3.4"
},
"bin": {
"ps-tree": "bin/ps-tree.js"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -16834,14 +17222,64 @@
"integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA=="
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz",
"integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
"glob": "^10.2.5"
},
"bin": {
"rimraf": "bin.js"
"rimraf": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.5.tgz",
"integrity": "sha512-bYUpUD7XDEHI4Q2O5a7PXGvyw4deKR70kHiDxzQbe925wbZknhOzUt2xBgTkYL6RBcVeXYuD9iNYeqoWbBZQnA==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.0.3",
"minimatch": "^9.0.1",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
"path-scurry": "^1.10.1"
},
"bin": {
"glob": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -16986,6 +17424,14 @@
"resolved": "https://registry.npmjs.org/rusha/-/rusha-0.8.14.tgz",
"integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA=="
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -17361,6 +17807,17 @@
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz",
"integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w=="
},
"node_modules/split": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
"integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==",
"dependencies": {
"through": "2"
},
"engines": {
"node": "*"
}
},
"node_modules/split2": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
@@ -17392,6 +17849,121 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/start-server-and-test": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.1.tgz",
"integrity": "sha512-8PFo4DLLLCDMuS51/BEEtE1m9CAXw1LNVtZSS1PzkYQh6Qf9JUwM4huYeSoUumaaoAyuwYBwCa9OsrcpMqcOdQ==",
"dependencies": {
"arg": "^5.0.2",
"bluebird": "3.7.2",
"check-more-types": "2.24.0",
"debug": "4.3.4",
"execa": "5.1.1",
"lazy-ass": "1.6.0",
"ps-tree": "1.2.0",
"wait-on": "7.0.1"
},
"bin": {
"server-test": "src/bin/start.js",
"start-server-and-test": "src/bin/start.js",
"start-test": "src/bin/start.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/start-server-and-test/node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
},
"node_modules/start-server-and-test/node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
},
"node_modules/start-server-and-test/node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
"human-signals": "^2.1.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1",
"onetime": "^5.1.2",
"signal-exit": "^3.0.3",
"strip-final-newline": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/start-server-and-test/node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"engines": {
"node": ">=10.17.0"
}
},
"node_modules/start-server-and-test/node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/start-server-and-test/node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"engines": {
"node": ">=6"
}
},
"node_modules/start-server-and-test/node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"dependencies": {
"path-key": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/start-server-and-test/node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/start-server-and-test/node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"engines": {
"node": ">=6"
}
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -17400,6 +17972,14 @@
"node": ">= 0.6"
}
},
"node_modules/stream-combiner": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
"integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==",
"dependencies": {
"duplexer": "~0.1.1"
}
},
"node_modules/stream-shift": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
@@ -17452,6 +18032,27 @@
"node": ">=8"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -17541,6 +18142,19 @@
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -17911,8 +18525,7 @@
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"dev": true
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"node_modules/through2": {
"version": "4.0.2",
@@ -19173,6 +19786,24 @@
"d3-timer": "^3.0.1"
}
},
"node_modules/wait-on": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz",
"integrity": "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==",
"dependencies": {
"axios": "^0.27.2",
"joi": "^17.7.0",
"lodash": "^4.17.21",
"minimist": "^1.2.7",
"rxjs": "^7.8.0"
},
"bin": {
"wait-on": "bin/wait-on"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@@ -19308,6 +19939,24 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -19476,6 +20125,7 @@
"@aws-sdk/signature-v4-crt": "^3.410.0",
"@documenso/email": "*",
"@documenso/prisma": "*",
"@documenso/signing": "*",
"@next-auth/prisma-adapter": "1.0.7",
"@pdf-lib/fontkit": "^1.1.1",
"@scure/base": "^1.1.3",
@@ -19526,6 +20176,26 @@
"typescript": "^5.1.6"
}
},
"packages/signing": {
"name": "@documenso/signing",
"version": "1.0.0",
"license": "AGPLv3",
"dependencies": {
"@documenso/tsconfig": "*",
"node-forge": "^1.3.1",
"node-signpdf": "^2.0.0",
"pdf-lib": "^1.17.1",
"ts-pattern": "^5.0.5"
},
"devDependencies": {
"@types/node-forge": "^1.3.4"
}
},
"packages/signing/node_modules/ts-pattern": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.0.5.tgz",
"integrity": "sha512-tL0w8U/pgaacOmkb9fRlYzWEUDCfVjjv9dD4wHTgZ61MjhuMt46VNWTG747NqW6vRzoWIKABVhFSOJ82FvXrfA=="
},
"packages/tailwind-config": {
"name": "@documenso/tailwind-config",
"version": "0.0.0",
@@ -19593,13 +20263,13 @@
"@radix-ui/react-tabs": "^1.0.3",
"@radix-ui/react-toast": "^1.1.3",
"@radix-ui/react-toggle": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.5",
"@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-table": "^8.9.1",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"cmdk": "^0.2.0",
"framer-motion": "^10.12.8",
"lucide-react": "^0.277.0",
"lucide-react": "^0.279.0",
"luxon": "^3.4.2",
"next": "13.4.19",
"pdfjs-dist": "3.6.172",
+5 -1
View File
@@ -4,10 +4,13 @@
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/web --filter=@documenso/marketing",
"start": "cd apps && cd web && next start",
"test": "cd apps && cd web && npm run test:e2e",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
"prepare": "husky install",
"commitlint": "commitlint --edit"
"commitlint": "commitlint --edit",
"ci": "turbo run build test:e2e lint",
"clean": "turbo run clean && rimraf node_modules"
},
"engines": {
"npm": ">=8.6.0",
@@ -23,6 +26,7 @@
"husky": "^8.0.0",
"lint-staged": "^14.0.0",
"prettier": "^2.5.1",
"rimraf": "^5.0.1",
"turbo": "^1.9.3"
},
"name": "@documenso/root",
+3 -1
View File
@@ -9,7 +9,9 @@
"server-only/",
"universal/"
],
"scripts": {},
"scripts": {
"clean": "rimraf node_modules"
},
"dependencies": {
"@documenso/lib": "*",
"@documenso/prisma": "*"
+1
View File
@@ -13,6 +13,7 @@
],
"scripts": {
"dev": "email dev --port 3002 --dir templates",
"clean": "rimraf node_modules",
"worker:test": "tsup worker/index.ts --format esm"
},
"dependencies": {
@@ -1,4 +1,4 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
@@ -27,11 +27,23 @@ export const TemplateDocumentCompleted = ({
},
}}
>
<Section className="flex-row items-center justify-center">
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Section>
<Row className="table-fixed">
<Column />
<Column>
<Img
className="h-42 mx-auto"
src={getAssetUrl('/static/document.png')}
alt="Documenso"
/>
</Column>
<Column />
</Row>
</Section>
<Section>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Completed
@@ -1,4 +1,4 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
@@ -30,13 +30,26 @@ export const TemplateDocumentInvite = ({
},
}}
>
<Section className="mt-4 flex-row items-center justify-center">
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Section className="mt-4">
<Row className="table-fixed">
<Column />
<Column>
<Img
className="h-42 mx-auto"
src={getAssetUrl('/static/document.png')}
alt="Documenso"
/>
</Column>
<Column />
</Row>
</Section>
<Section>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
{inviterName} has invited you to sign "{documentName}"
{inviterName} has invited you to sign
<br />"{documentName}"
</Text>
<Text className="my-1 text-center text-base text-slate-400">

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