mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
28 Commits
fix/docume
...
v1.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b8914da83 | |||
| 29910ab2a7 | |||
| ef3ecc33f1 | |||
| 0244f021ab | |||
| e5f73452b3 | |||
| c605877924 | |||
| e0065a8731 | |||
| f74265850b | |||
| 909c38f47e | |||
| 1beb434a72 | |||
| 5582f29bda | |||
| 7ed0a909eb | |||
| a9025b5d97 | |||
| 0c744a1123 | |||
| 0f86eed6ac | |||
| 4b485268ca | |||
| f31caaab08 | |||
| 71a8a3eaa6 | |||
| 0ff3844f18 | |||
| 3421515452 | |||
| 1028184cf2 | |||
| 277a870580 | |||
| a8febae87e | |||
| b366ab8736 | |||
| 994f6867f5 | |||
| c2374a9d65 | |||
| 7a1b9feee3 | |||
| ddc704518f |
@ -10,12 +10,19 @@ NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE"
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
|
||||
|
||||
# [[AUTH OPTIONAL]]
|
||||
# Find documentation on setting up Google OAuth here:
|
||||
# https://docs.documenso.com/developers/self-hosting/setting-up-oauth-providers#google-oauth-gmail
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
|
||||
|
||||
NEXT_PRIVATE_OIDC_WELL_KNOWN=""
|
||||
NEXT_PRIVATE_OIDC_CLIENT_ID=""
|
||||
NEXT_PRIVATE_OIDC_CLIENT_SECRET=""
|
||||
NEXT_PRIVATE_OIDC_PROVIDER_LABEL="OIDC"
|
||||
# This can be used to still allow signups for OIDC connections
|
||||
# when signup is disabled via `NEXT_PUBLIC_DISABLE_SIGNUP`
|
||||
NEXT_PRIVATE_OIDC_ALLOW_SIGNUP=""
|
||||
NEXT_PRIVATE_OIDC_SKIP_VERIFY=""
|
||||
|
||||
# [[URLS]]
|
||||
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
||||
|
||||
@ -11,4 +11,5 @@ module.exports = {
|
||||
rootDir: ['apps/*/'],
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['lingui.config.ts', 'packages/lib/translations/**/*.js'],
|
||||
};
|
||||
|
||||
38
.github/workflows/translations-extract.yml
vendored
Normal file
38
.github/workflows/translations-extract.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
# Extract and compile translations for all PRs.
|
||||
|
||||
name: 'Extract and compile translations'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
extract_translations:
|
||||
name: Extract and compile translations
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- name: Extract and compile translations
|
||||
run: |
|
||||
npm run translate:extract
|
||||
npm run translate:compile
|
||||
|
||||
- name: Check and commit any files created
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@documenso.com'
|
||||
git add packages/lib/translations
|
||||
git diff --staged --quiet --exit-code || (git commit -m "chore: extract translations" && git push)
|
||||
51
.github/workflows/translations-force-pull.yml
vendored
Normal file
51
.github/workflows/translations-force-pull.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# This is similar to the "Pull Translations" workflow, but without the conditional check to allow us to
|
||||
# forcefully pull down translations from Crowdin and create a PR regardless if all the translations are fulfilled.
|
||||
#
|
||||
# Intended to be used when we manually update translations in Crowdin UI and want to pull those down when
|
||||
# they already exist.
|
||||
|
||||
name: 'Force pull translations'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pull_translations:
|
||||
name: Force pull translations
|
||||
runs-on: ubuntu-latest
|
||||
environment: Translations
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- name: Pull translations from Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
export_only_approved: false
|
||||
localization_branch_name: chore/translations
|
||||
commit_message: 'chore: add translations'
|
||||
pull_request_title: 'chore: add translations'
|
||||
|
||||
env:
|
||||
# A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository).
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||
|
||||
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
|
||||
# Visit https://crowdin.com/settings#api-key to create this token
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
8
.github/workflows/translations-pull.yml
vendored
8
.github/workflows/translations-pull.yml
vendored
@ -3,11 +3,10 @@
|
||||
name: 'Pull translations'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */2 * * *' # Every two hours.
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
# Cron disabled until i18n PR is landed.
|
||||
# schedule:
|
||||
# - cron: '0 */2 * * *' # Every two hours.
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
@ -17,6 +16,7 @@ jobs:
|
||||
pull_translations:
|
||||
name: Pull translations
|
||||
runs-on: ubuntu-latest
|
||||
environment: Translations
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
env:
|
||||
# A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository).
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_CROWDIN_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||
|
||||
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
|
||||
6
.github/workflows/translations-upload.yml
vendored
6
.github/workflows/translations-upload.yml
vendored
@ -3,9 +3,8 @@ name: 'Extract and upload translations'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
# Disabled until i18n PR is landed.
|
||||
# push:
|
||||
# branches: ['main']
|
||||
push:
|
||||
branches: ['main']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
@ -15,6 +14,7 @@ jobs:
|
||||
extract_translations:
|
||||
name: Extract and upload translations
|
||||
runs-on: ubuntu-latest
|
||||
environment: Translations
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
|
||||
@ -13,4 +13,9 @@ node "$MONOREPO_ROOT/scripts/copy-wellknown.cjs"
|
||||
git add "$MONOREPO_ROOT/apps/web/public/"
|
||||
git add "$MONOREPO_ROOT/apps/marketing/public/"
|
||||
|
||||
echo "Extract and compile translations"
|
||||
npm run translate:extract
|
||||
npm run translate:compile
|
||||
git add "$MONOREPO_ROOT/packages/lib/translations/"
|
||||
|
||||
npx lint-staged
|
||||
|
||||
@ -4,6 +4,7 @@ public
|
||||
**/**/node_modules
|
||||
**/**/.next
|
||||
**/**/public
|
||||
packages/lib/translations/**/*.js
|
||||
|
||||
*.lock
|
||||
*.log
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"index": "Getting Started",
|
||||
"signing-certificate": "Signing Certificate",
|
||||
"how-to": "How To"
|
||||
}
|
||||
"how-to": "How To",
|
||||
"setting-up-oauth-providers": "Setting up OAuth Providers"
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Setting up OAuth Providers
|
||||
description: Learn how to set up OAuth providers for your own instance of Documenso.
|
||||
---
|
||||
|
||||
## Google OAuth (Gmail)
|
||||
|
||||
To use Google OAuth, you will need to create a Google Cloud Platform project and enable the Google Identity and Access Management (IAM) API. You will also need to create a new OAuth client ID and download the client secret.
|
||||
|
||||
### Create and configure a new OAuth client ID
|
||||
|
||||
1. Go to the [Google Cloud Platform Console](https://console.cloud.google.com/)
|
||||
2. From the projects list, select a project or create a new one
|
||||
3. If the APIs & services page isn't already open, open the console left side menu and select APIs & services
|
||||
4. On the left, click Credentials
|
||||
5. Click New Credentials, then select OAuth client ID
|
||||
6. When prompted to select an application type, select Web application
|
||||
7. Enter a name for your client ID, and click Create
|
||||
8. Click the download button to download the client secret
|
||||
9. Set the authorized javascript origins to `https://<documenso-domain>`
|
||||
10. Set the authorized redirect URIs to `https://<documenso-domain>/api/auth/callback/google`
|
||||
11. In the Documenso environment variables, set the following:
|
||||
|
||||
```
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_ID=<your-client-id>
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=<your-client-secret>
|
||||
```
|
||||
|
||||
Finally verify the signing in with Google works by signing in with your Google account and checking the email address in your profile.
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"index": "Introduction",
|
||||
"support": "Support",
|
||||
"-- How To Use": {
|
||||
"type": "separator",
|
||||
"title": "How To Use"
|
||||
@ -13,6 +14,7 @@
|
||||
"type": "separator",
|
||||
"title": "Legal Overview"
|
||||
},
|
||||
"fair-use": "Fair Use Policy",
|
||||
"licenses": "Licenses",
|
||||
"compliance": "Compliance"
|
||||
}
|
||||
|
||||
@ -10,6 +10,8 @@ import { Callout } from 'nextra/components';
|
||||
signatures to ensure their authenticity, integrity, and confidentiality in the pharmaceutical, medical
|
||||
device, and other FDA-regulated industries.
|
||||
|
||||
> Read more about 21 CFR Part 11 with Documenso here: https://documen.so/21-CFR-Part-11
|
||||
|
||||
### Main Requirements
|
||||
|
||||
- [x] Strong Identity Checks for each Signature
|
||||
|
||||
34
apps/documentation/pages/users/fair-use.mdx
Normal file
34
apps/documentation/pages/users/fair-use.mdx
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Fair Use Policy
|
||||
description: Learn about our fair use policy, which enables us to have unlimited plans.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Fair Use Policy
|
||||
|
||||
### Why
|
||||
|
||||
We offer our plans without any limits on volume because we want our users and customers to make the most of their accounts. Estimating volume is incredibly hard, especially for shorter intervals like a quarter. We are not interested in selling volume packages our customers end up not using. This is why the individual plan and the team plan do not include a limit on signing or API volume. If you are a customer of these [plans](https://documen.so/pricing), we ask you to abide by this fair use policy:
|
||||
|
||||
### Spirit of the Plan
|
||||
|
||||
> Use the limitless accounts as much as you like (they are meant to offer a lot) while respecting the spirit and intended scope of the account.
|
||||
|
||||
<Callout type="info">
|
||||
What happens if I violate this policy? We will ask you to upgrade to a fitting plan or custom
|
||||
pricing. We won’t block your account without reaching out. [Message
|
||||
us](mailto:support@documenso.com) for questions. It's probably fine, though.
|
||||
</Callout>
|
||||
|
||||
### DO
|
||||
|
||||
- Sign as many documents with the individual plan for your single business or organization you are part of
|
||||
- Use the API and Zapier to automate all your signing to sign as much as possible
|
||||
- Experiment with the plans and integrations, testing what you want to build: When in doubt, do it. Especially if you are just starting.
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use the individual account's API to power a platform
|
||||
- Run a huge company, signing thousands of documents per day on a two-user team plan using the API
|
||||
- Let this policy make you overthink. If you are a paying customer, we want you to win, and it's probably fine
|
||||
@ -3,7 +3,7 @@ title: Create Your Account
|
||||
description: Learn how to create an account on Documenso.
|
||||
---
|
||||
|
||||
import { Steps } from 'nextra/components';
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
# Create Your Account
|
||||
|
||||
@ -14,6 +14,8 @@ The first step to start using Documenso is to pick a plan and create an account.
|
||||
|
||||
Explore each plan's features and choose the one that best suits your needs. The [pricing page](https://documen.so/pricing) has more information about the plans.
|
||||
|
||||
<Callout>All plans are subject to our [Fair Use Policy](/users/fair-use).</Callout>
|
||||
|
||||
### Create an account
|
||||
|
||||
If you are unsure which plan to choose, you can start with the free plan and upgrade later.
|
||||
|
||||
38
apps/documentation/pages/users/support.mdx
Normal file
38
apps/documentation/pages/users/support.mdx
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Support
|
||||
description: Learn what types of support we offer.
|
||||
---
|
||||
|
||||
# Support
|
||||
|
||||
## Community Support
|
||||
|
||||
If you are a developer or free user, you can reach out to the community or raise an issue:
|
||||
|
||||
### [Create Github Issues](https://github.com/documenso/documenso/issues)
|
||||
|
||||
The community and the core team address GitHub issues. Be sure to check if a similar issue already exists. Please note that while we want to address everything immediately, we must prioritize.
|
||||
|
||||
### [Join our Discord](https://documen.so/discord)
|
||||
|
||||
You can ask for help in the [community help channel](https://discord.com/channels/1132216843537485854/1133419426524430376).
|
||||
|
||||
## Paid Account Support
|
||||
|
||||
### Email: support@documenso.com
|
||||
|
||||
If you are paying customers facing issues, email our customer support, especially in urgent cases.
|
||||
|
||||
### Private Discord channel
|
||||
|
||||
If you prefer Discord, we can invite you to a private channel. Message support to make this happen.
|
||||
|
||||
## Enterprise Support
|
||||
|
||||
### Email: support@documenso.com
|
||||
|
||||
If you are paying customers facing issues, email our customer support, especially in urgent cases.
|
||||
|
||||
### Slack
|
||||
|
||||
If your team is on Slack, we can create a private workspace to support you more closely.
|
||||
@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Creating an Efficient Statement of Work Approval Process with Documenso
|
||||
description: Submitting statements of work can be a drag on morale and project efficiency. Let's look at how to create a modern, low-friction workflow for this.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-07-23
|
||||
tags:
|
||||
- Freelancer
|
||||
- Statement of Work
|
||||
- Productivity
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/sov.webp"
|
||||
width="1400"
|
||||
height="884"
|
||||
alt="Working papers image"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">Fine-tune your process using custom role for everyone involved.</figcaption>
|
||||
</figure>
|
||||
|
||||
> TLDR; Statements of Work detail what needs to be done. Automate sending and approving them using Documenso and Zapier.
|
||||
|
||||
## What is a Statement of Work
|
||||
|
||||
A statement of work is a detailed document that outlines what needs to be done in a project. It covers the project's scope, objectives, and deliverables, laying out all the tasks, deadlines, and milestones. The statement of work also spells out who’s responsible for what, ensuring everyone’s on the same page. It’s a roadmap that keeps both clients and service providers aligned and ensures the project stays on track from start to finish.
|
||||
|
||||
In the context of freelance work, the statement of work is a document that outlines the details of a project between a freelancer and their client. It's a concrete work to be agreed upon and tracked after completion. The statement of work is created after the [proposal is accepted](https://documen.so/freelance-proposal) and the [contract signed](https://documen.so/freelance-contract).
|
||||
|
||||
## What does a good workflow look like?
|
||||
|
||||
### 1. Create the statement of work
|
||||
|
||||
The team at Zapier created a [excellent guide](https://zapier.com/blog/statement-of-work-template/), which goes into the statement of work. There is a short checklist:
|
||||
|
||||
- Project Context/ Current Scope
|
||||
- Objectives for this piece of work
|
||||
- Scope (tasks, activities, and limits)
|
||||
- Requirements (e.g., technical and regulatory)
|
||||
- Deliverables to be created
|
||||
- Roles and Responsibilities
|
||||
|
||||
### 2. Get approval from subject matter experts (optional)
|
||||
|
||||
Since a statements of work can be very technical, having professionals from either side approve it first can be sensible. This can avoid double or unnecessary work and minimize the chances of misunderstandings. If this makes sense, it depends heavily on the scale of the project and the level of insight of the professional providing it.
|
||||
|
||||
### 3. Let the client sign off
|
||||
|
||||
Having the client sign off on the concrete work is the central step of the statement of work workflow. Assuming the document’s content is correct, getting the go-ahead ensures everyone is aligned and clear on what should and will be worked on.
|
||||
|
||||
### 4. Inform other Stakeholders (optional)
|
||||
|
||||
Depending on the scale of the organizations working together, other people may need to be kept in the loop. This could be accounting on either side, project managers, or other interested parties.
|
||||
|
||||
## Fine-Tuning the Flow with Custom Roles
|
||||
|
||||
<figure>
|
||||
<MdxNextImage src="/blog/roles.webp" width="1400" height="884" alt="Documenso Roles UI" />
|
||||
<figcaption className="text-center">
|
||||
Let's take a look at what it would look like with Documenso.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
### 1. Creating the Document: Templates vs. Custom Document
|
||||
|
||||
[Creating a template](https://docs.documenso.com/users/templates) can make sense if you submit statements of work regularly. If you create a Documenso Template, you can add [dynamic text and number fields](https://docs.documenso.com/users/signing-documents/fields) to be filled out when using the template. Another approach to this is creating a template on a document service like Google Docs, filling out a new copy, and uploading the custom-created document to Documenso.
|
||||
|
||||
Different parts of this process can be automated using the [Zapier Documenso Integration](https://documen.so/zapier) as desired:
|
||||
|
||||
- Automatically sending out a template to be filled out and signed
|
||||
- Creating a document in Documenso from a newly created document in Google Docs
|
||||
- Triggering sending a document created from either template or automation
|
||||
|
||||
### 2. Approvals
|
||||
|
||||
Looping in subject matter experts can easily be done using the approver role. This role allows you to complete a document without blocking the signing. This means the client can sign a document, even if the approval is not yet in place, removing friction. However, having the approval denied will stop the document from being completed and let everyone know there are corrections to be made. A software version of this is possible, having the expert in a viewer role and marking the document as seen without the option to block.
|
||||
|
||||
### 3. Signers
|
||||
|
||||
Looping in the client is done by adding one or more signer roles. Signer roles require recipients to place at least one signature to fulfill their part in the flow.roles
|
||||
|
||||
### 4. BCC
|
||||
|
||||
You can add one or several BCC roles to inform interested parties, e.g., accounting or project managers. As the process finishes, BCC recipients receive a copy of the completed document (assuming it completes and is not blocked). Using BCC roles automates filling in everyone who is only interested in the outcome and wants to avoid involvement in the steps leading up to it.
|
||||
|
||||
### Conclusion
|
||||
|
||||
Streamlining your statement of work approval process with Documenso can significantly improve productivity and ease. Using templates, dynamic fields, and Zapier integrations, you can create a smooth, efficient workflow from start to finish. Adding roles for experts, signers, and BCC recipients tailors the process to fit your project's needs, ensuring everyone stays on the same page.
|
||||
|
||||
An efficient SOW process saves time and improves communication, letting you focus on delivering great work. We're excited to hear your thoughts and experiences—reach out on [Twitter / X](https://twitter.com/eltimuro) (DMs are open) or [Discord](https://documen.so/discord) if you have any questions, ideas, or comments.
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
@ -8,7 +8,50 @@ Check out what's new in the latest version and read our thoughts on it. For more
|
||||
|
||||
---
|
||||
|
||||
## v1.5.6 (latest)
|
||||
## v1.6.0: Enhancing Team Collaboration and User Experience (latest)
|
||||
|
||||
### <small>Released 23th July 2024</small>
|
||||
|
||||
> This release contains [8 fixes](https://github.com/documenso/documenso/releases/tag/v1.6.0)
|
||||
|
||||
We're excited to announce the release of Documenso v1.6.0! The new release is packed with new features and improvements to streamline the signing process. Here are the highlights:
|
||||
|
||||
### 📚 New Documentation Site
|
||||
|
||||
We’ve launched a comprehensive documentation site to help you make the most of Documenso. Check it out to explore all our features and best practices! Based on your feedback, we’re constantly working to improve Documenso. Feel free to ping us to update the docs or even raise PRs.
|
||||
|
||||
> Check out the docs at [https://docs.documenso.com](https://docs.documenso.com)
|
||||
|
||||
### ✅ Enhanced Field Types
|
||||
|
||||
We've significantly expanded our field options to give you more document and template creation flexibility. New field types include:
|
||||
|
||||
- Dropdown Menus
|
||||
- Checkboxes
|
||||
- Radio Buttons
|
||||
- Number Fields
|
||||
|
||||
We've added more customization options for each field type, allowing you to create more dynamic and interactive documents. All non-auto field types allow setting pre-fill values, placeholders, and labels. For more details, see the new [documentation](https://docs.documenso.com/users/signing-documents/fields).
|
||||
|
||||
### 🪪 Public Profiles
|
||||
|
||||
We've introduced [Public Profiles](http://localhost:3001/blog/announcing-profiles), allowing you to showcase your professional identity within Documenso and showcase your publicly available templates. This feature enhances transparency and trust in your signing processes. This feature enhances transparency and trust in your signing processes.
|
||||
|
||||
### ⬅️ Move Documents and Templates to Teams
|
||||
|
||||
Did you accidentally create a document under your personal account? No problem! You can move documents and templates between your personal account and team workspaces, facilitating better organization and teamwork.
|
||||
|
||||
### 🔧 Other Improvements
|
||||
|
||||
- Background Tasks: We've implemented a system for handling background tasks, improving overall performance and responsiveness.
|
||||
- Force Signature Fields: Document creators can now ensure that signers complete all required signature fields.
|
||||
- Custom Emails for Direct Template Documents: Personalize your communication by sending custom emails to signers of direct template documents.
|
||||
- API Enhancements: We've added more template API endpoints and the ability to resend documents via API, giving developers more flexibility.
|
||||
- Anonymous SMTP Authentication: We now support anonymous SMTP authentication for those who need it.
|
||||
|
||||
---
|
||||
|
||||
## v1.5.6
|
||||
|
||||
### <small>Released 28th June 2024</small>
|
||||
|
||||
|
||||
22
apps/marketing/lingui.config.ts
Normal file
22
apps/marketing/lingui.config.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { LinguiConfig } from '@lingui/conf';
|
||||
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
|
||||
// Extends root lingui.config.cjs.
|
||||
const config: LinguiConfig = {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
locales: APP_I18N_OPTIONS.supportedLangs as unknown as string[],
|
||||
catalogs: [
|
||||
{
|
||||
path: '<rootDir>/../../packages/lib/translations/{locale}/marketing',
|
||||
include: ['<rootDir>/apps/marketing/src'],
|
||||
},
|
||||
{
|
||||
path: '<rootDir>/../../packages/lib/translations/{locale}/common',
|
||||
include: ['<rootDir>/packages/ui', '<rootDir>/packages/lib'],
|
||||
},
|
||||
],
|
||||
catalogsMergePath: '<rootDir>/../../packages/lib/translations/{locale}/marketing',
|
||||
};
|
||||
|
||||
export default config;
|
||||
@ -30,6 +30,7 @@ const config = {
|
||||
serverActions: {
|
||||
bodySizeLimit: '50mb',
|
||||
},
|
||||
swcPlugins: [['@lingui/swc-plugin', {}]],
|
||||
},
|
||||
reactStrictMode: true,
|
||||
transpilePackages: [
|
||||
@ -55,6 +56,13 @@ const config = {
|
||||
config.resolve.alias.canvas = false;
|
||||
}
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.po$/,
|
||||
use: {
|
||||
loader: '@lingui/loader',
|
||||
},
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
async headers() {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -10,7 +10,8 @@
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"clean": "rimraf .next && rimraf node_modules",
|
||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs",
|
||||
"translate:compile": "lingui compile --typescript"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/assets": "*",
|
||||
@ -19,7 +20,10 @@
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@lingui/macro": "^4.11.1",
|
||||
"@lingui/react": "^4.11.1",
|
||||
"@openstatus/react": "^0.0.3",
|
||||
"cmdk": "^0.2.1",
|
||||
"contentlayer": "^0.3.4",
|
||||
"embla-carousel": "^8.1.3",
|
||||
"embla-carousel-autoplay": "^8.1.3",
|
||||
@ -46,6 +50,8 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/loader": "^4.11.1",
|
||||
"@lingui/swc-plugin": "4.0.6",
|
||||
"@types/node": "20.1.0",
|
||||
"@types/react": "18.2.18",
|
||||
"@types/react-dom": "18.2.7"
|
||||
|
||||
BIN
apps/marketing/public/blog/roles.webp
Normal file
BIN
apps/marketing/public/blog/roles.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@ -1,12 +1,17 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { allBlogPosts } from 'contentlayer/generated';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog',
|
||||
};
|
||||
|
||||
export default function BlogPage() {
|
||||
const { i18n } = setupI18nSSR();
|
||||
|
||||
const blogPosts = allBlogPosts.sort((a, b) => {
|
||||
const dateA = new Date(a.date);
|
||||
const dateB = new Date(b.date);
|
||||
@ -17,11 +22,15 @@ export default function BlogPage() {
|
||||
return (
|
||||
<div className="mt-6 sm:mt-12">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">From the blog</h1>
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">
|
||||
<Trans>From the blog</Trans>
|
||||
</h1>
|
||||
|
||||
<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!
|
||||
<Trans>
|
||||
Get the latest news from Documenso, including product updates, team announcements and
|
||||
more!
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -33,7 +42,7 @@ export default function BlogPage() {
|
||||
>
|
||||
<div className="flex items-center gap-x-4 text-xs">
|
||||
<time dateTime={post.date} className="text-muted-foreground">
|
||||
{new Date(post.date).toLocaleDateString()}
|
||||
<Trans>{i18n.date(new Date(), { dateStyle: 'short' })}</Trans>
|
||||
</time>
|
||||
|
||||
{post.tags.length > 0 && (
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
export const TEAM_MEMBERS = [
|
||||
{
|
||||
name: 'Timur Ercan',
|
||||
role: 'Co-Founder, CEO',
|
||||
salary: 95_000,
|
||||
location: 'Germany',
|
||||
engagement: 'Full-Time',
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'November 14th, 2022',
|
||||
},
|
||||
{
|
||||
@ -12,7 +14,7 @@ export const TEAM_MEMBERS = [
|
||||
role: 'Co-Founder, CTO',
|
||||
salary: 95_000,
|
||||
location: 'Australia',
|
||||
engagement: 'Full-Time',
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'April 19th, 2023',
|
||||
},
|
||||
{
|
||||
@ -20,7 +22,7 @@ export const TEAM_MEMBERS = [
|
||||
role: 'Software Engineer - Intern',
|
||||
salary: 15_000,
|
||||
location: 'Ghana',
|
||||
engagement: 'Part-Time',
|
||||
engagement: msg`Part-Time`,
|
||||
joinDate: 'June 6th, 2023',
|
||||
},
|
||||
{
|
||||
@ -28,7 +30,7 @@ export const TEAM_MEMBERS = [
|
||||
role: 'Software Engineer - III',
|
||||
salary: 100_000,
|
||||
location: 'Australia',
|
||||
engagement: 'Full-Time',
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'July 26th, 2023',
|
||||
},
|
||||
{
|
||||
@ -36,7 +38,7 @@ export const TEAM_MEMBERS = [
|
||||
role: 'Software Engineer - II',
|
||||
salary: 80_000,
|
||||
location: 'Romania',
|
||||
engagement: 'Full-Time',
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'September 4th, 2023',
|
||||
},
|
||||
{
|
||||
@ -44,7 +46,7 @@ export const TEAM_MEMBERS = [
|
||||
role: 'Designer - III',
|
||||
salary: 100_000,
|
||||
location: 'India',
|
||||
engagement: 'Full-Time',
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'October 9th, 2023',
|
||||
},
|
||||
];
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
||||
@ -11,6 +13,8 @@ export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
|
||||
};
|
||||
|
||||
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const formattedData = data.map((item) => ({
|
||||
amount: Number(item.amount),
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
@ -21,7 +25,9 @@ export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps)
|
||||
<div className={className} {...props}>
|
||||
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">Total Funding Raised</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>Total Funding Raised</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@ -49,14 +55,14 @@ export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps)
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 0,
|
||||
}),
|
||||
'Amount Raised',
|
||||
_(msg`Amount Raised`),
|
||||
]}
|
||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="amount"
|
||||
fill="hsl(var(--primary))"
|
||||
label="Amount Raised"
|
||||
label={_(msg`Amount Raised`)}
|
||||
maxBarSize={60}
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
@ -14,6 +16,8 @@ export const MonthlyCompletedDocumentsChart = ({
|
||||
className,
|
||||
data,
|
||||
}: MonthlyCompletedDocumentsChartProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const formattedData = [...data].reverse().map(({ month, count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
|
||||
@ -25,7 +29,9 @@ export const MonthlyCompletedDocumentsChart = ({
|
||||
<div className={className}>
|
||||
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">Completed Documents per Month</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>Completed Documents per Month</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@ -46,7 +52,7 @@ export const MonthlyCompletedDocumentsChart = ({
|
||||
fill="hsl(var(--primary))"
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={60}
|
||||
label="Completed Documents"
|
||||
label={_(msg`Completed Documents`)}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
@ -11,6 +13,8 @@ export type MonthlyNewUsersChartProps = {
|
||||
};
|
||||
|
||||
export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const formattedData = [...data].reverse().map(({ month, count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
|
||||
@ -22,7 +26,9 @@ export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartPr
|
||||
<div className={className}>
|
||||
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">New Users</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>New Users</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@ -34,7 +40,7 @@ export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartPr
|
||||
labelStyle={{
|
||||
color: 'hsl(var(--primary-foreground))',
|
||||
}}
|
||||
formatter={(value) => [Number(value).toLocaleString('en-US'), 'New Users']}
|
||||
formatter={(value) => [Number(value).toLocaleString('en-US'), _(msg`New Users`)]}
|
||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||
/>
|
||||
|
||||
@ -43,7 +49,7 @@ export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartPr
|
||||
fill="hsl(var(--primary))"
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={60}
|
||||
label="New Users"
|
||||
label={_(msg`New Users`)}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
@ -11,6 +13,8 @@ export type MonthlyTotalUsersChartProps = {
|
||||
};
|
||||
|
||||
export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersChartProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const formattedData = [...data].reverse().map(({ month, cume_count: count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
|
||||
@ -22,7 +26,9 @@ export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersCha
|
||||
<div className={className}>
|
||||
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">Total Users</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>Total Users</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@ -34,7 +40,7 @@ export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersCha
|
||||
labelStyle={{
|
||||
color: 'hsl(var(--primary-foreground))',
|
||||
}}
|
||||
formatter={(value) => [Number(value).toLocaleString('en-US'), 'Total Users']}
|
||||
formatter={(value) => [Number(value).toLocaleString('en-US'), _(msg`Total Users`)]}
|
||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||
/>
|
||||
|
||||
@ -43,7 +49,7 @@ export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersCha
|
||||
fill="hsl(var(--primary))"
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={60}
|
||||
label="Total Users"
|
||||
label={_(msg`Total Users`)}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getCompletedDocumentsMonthly } from '@documenso/lib/server-only/user/get-monthly-completed-document';
|
||||
import { getUserMonthlyGrowth } from '@documenso/lib/server-only/user/get-user-monthly-growth';
|
||||
|
||||
@ -128,6 +131,10 @@ const fetchEarlyAdopters = async () => {
|
||||
};
|
||||
|
||||
export default async function OpenPage() {
|
||||
setupI18nSSR();
|
||||
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [
|
||||
{ forks_count: forksCount, stargazers_count: stargazersCount },
|
||||
{ total_count: openIssues },
|
||||
@ -150,19 +157,23 @@ export default async function OpenPage() {
|
||||
<div>
|
||||
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">
|
||||
<Trans>Open Startup</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[60ch] text-center text-lg leading-normal">
|
||||
All our metrics, finances, and learnings are public. We believe in transparency and want
|
||||
to share our journey with you. You can read more about why here:{' '}
|
||||
<a
|
||||
className="font-bold"
|
||||
href="https://documenso.com/blog/pre-seed"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Announcing Open Metrics
|
||||
</a>
|
||||
<Trans>
|
||||
All our metrics, finances, and learnings are public. We believe in transparency and
|
||||
want to share our journey with you. You can read more about why here:{' '}
|
||||
<a
|
||||
className="font-bold"
|
||||
href="https://documenso.com/blog/pre-seed"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Announcing Open Metrics
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -180,12 +191,12 @@ export default async function OpenPage() {
|
||||
/>
|
||||
<MetricCard
|
||||
className="col-span-2 lg:col-span-1"
|
||||
title="Open Issues"
|
||||
title={_(msg`Open Issues`)}
|
||||
value={openIssues.toLocaleString('en-US')}
|
||||
/>
|
||||
<MetricCard
|
||||
className="col-span-2 lg:col-span-1"
|
||||
title="Merged PR's"
|
||||
title={_(msg`Merged PR's`)}
|
||||
value={mergedPullRequests.toLocaleString('en-US')}
|
||||
/>
|
||||
</div>
|
||||
@ -195,28 +206,32 @@ export default async function OpenPage() {
|
||||
<SalaryBands className="col-span-12" />
|
||||
</div>
|
||||
|
||||
<h2 className="px-4 text-2xl font-semibold">Finances</h2>
|
||||
<h2 className="px-4 text-2xl font-semibold">
|
||||
<Trans>Finances</Trans>
|
||||
</h2>
|
||||
<div className="mb-12 mt-4 grid grid-cols-12 gap-8">
|
||||
<FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
|
||||
|
||||
<CapTable className="col-span-12 lg:col-span-6" />
|
||||
</div>
|
||||
|
||||
<h2 className="px-4 text-2xl font-semibold">Community</h2>
|
||||
<h2 className="px-4 text-2xl font-semibold">
|
||||
<Trans>Community</Trans>
|
||||
</h2>
|
||||
<div className="mb-12 mt-4 grid grid-cols-12 gap-8">
|
||||
<BarMetric<StargazersType>
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="stars"
|
||||
title="GitHub: Total Stars"
|
||||
label="Stars"
|
||||
title={_(msg`GitHub: Total Stars`)}
|
||||
label={_(msg`Stars`)}
|
||||
className="col-span-12 lg:col-span-6"
|
||||
/>
|
||||
|
||||
<BarMetric<StargazersType>
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="mergedPRs"
|
||||
title="GitHub: Total Merged PRs"
|
||||
label="Merged PRs"
|
||||
title={_(msg`GitHub: Total Merged PRs`)}
|
||||
label={_(msg`Merged PRs`)}
|
||||
chartHeight={400}
|
||||
className="col-span-12 lg:col-span-6"
|
||||
/>
|
||||
@ -233,8 +248,8 @@ export default async function OpenPage() {
|
||||
<BarMetric<StargazersType>
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="openIssues"
|
||||
title="GitHub: Total Open Issues"
|
||||
label="Open Issues"
|
||||
title={_(msg`GitHub: Total Open Issues`)}
|
||||
label={_(msg`Open Issues`)}
|
||||
chartHeight={400}
|
||||
className="col-span-12 lg:col-span-6"
|
||||
/>
|
||||
@ -242,13 +257,15 @@ export default async function OpenPage() {
|
||||
<Typefully className="col-span-12 lg:col-span-6" />
|
||||
</div>
|
||||
|
||||
<h2 className="px-4 text-2xl font-semibold">Growth</h2>
|
||||
<h2 className="px-4 text-2xl font-semibold">
|
||||
<Trans>Growth</Trans>
|
||||
</h2>
|
||||
<div className="mb-12 mt-4 grid grid-cols-12 gap-8">
|
||||
<BarMetric<EarlyAdoptersType>
|
||||
data={EARLY_ADOPTERS_DATA}
|
||||
metricKey="earlyAdopters"
|
||||
title="Total Customers"
|
||||
label="Total Customers"
|
||||
title={_(msg`Total Customers`)}
|
||||
label={_(msg`Total Customers`)}
|
||||
className="col-span-12 lg:col-span-6"
|
||||
extraInfo={<OpenPageTooltip />}
|
||||
/>
|
||||
@ -268,11 +285,15 @@ export default async function OpenPage() {
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
|
||||
<h2 className="text-2xl font-bold">Is there more?</h2>
|
||||
<h2 className="text-2xl font-bold">
|
||||
<Trans>Is there more?</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center text-lg leading-normal">
|
||||
This page is evolving as we learn what makes a great signing company. We'll update it when
|
||||
we have more to share.
|
||||
<Trans>
|
||||
This page is evolving as we learn what makes a great signing company. We'll update it
|
||||
when we have more to share.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import {
|
||||
Table,
|
||||
@ -17,15 +19,23 @@ export type SalaryBandsProps = HTMLAttributes<HTMLDivElement>;
|
||||
export const SalaryBands = ({ className, ...props }: SalaryBandsProps) => {
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)} {...props}>
|
||||
<h3 className="px-4 text-lg font-semibold">Global Salary Bands</h3>
|
||||
<h3 className="px-4 text-lg font-semibold">
|
||||
<Trans>Global Salary Bands</Trans>
|
||||
</h3>
|
||||
|
||||
<div className="border-border mt-2.5 flex-1 rounded-2xl border shadow-sm hover:shadow">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">Title</TableHead>
|
||||
<TableHead>Seniority</TableHead>
|
||||
<TableHead className="w-[100px] text-right">Salary</TableHead>
|
||||
<TableHead className="w-[200px]">
|
||||
<Trans>Title</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Seniority</Trans>
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px] text-right">
|
||||
<Trans>Salary</Trans>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import {
|
||||
Table,
|
||||
@ -15,20 +18,36 @@ import { TEAM_MEMBERS } from './data';
|
||||
export type TeamMembersProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const TeamMembers = ({ className, ...props }: TeamMembersProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)} {...props}>
|
||||
<h2 className="px-4 text-2xl font-semibold">Team</h2>
|
||||
<h2 className="px-4 text-2xl font-semibold">
|
||||
<Trans>Team</Trans>
|
||||
</h2>
|
||||
|
||||
<div className="border-border mt-2.5 flex-1 rounded-2xl border shadow-sm hover:shadow">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="">Name</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Salary</TableHead>
|
||||
<TableHead>Engagement</TableHead>
|
||||
<TableHead>Location</TableHead>
|
||||
<TableHead className="w-[100px] text-right">Join Date</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Name</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Role</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Salary</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Engagement</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Location</Trans>
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px] text-right">
|
||||
<Trans>Join Date</Trans>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@ -44,7 +63,7 @@ export const TeamMembers = ({ className, ...props }: TeamMembersProps) => {
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell>{member.engagement}</TableCell>
|
||||
<TableCell>{_(member.engagement)}</TableCell>
|
||||
<TableCell>{member.location}</TableCell>
|
||||
<TableCell className="text-right">{member.joinDate}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@ -29,7 +31,9 @@ export function OpenPageTooltip() {
|
||||
</svg>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Customers with an Active Subscriptions.</p>
|
||||
<p>
|
||||
<Trans>Customers with an Active Subscriptions.</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
@ -11,6 +13,8 @@ export type TotalSignedDocumentsChartProps = {
|
||||
};
|
||||
|
||||
export const TotalSignedDocumentsChart = ({ className, data }: TotalSignedDocumentsChartProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const formattedData = [...data].reverse().map(({ month, cume_count: count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
|
||||
@ -22,7 +26,9 @@ export const TotalSignedDocumentsChart = ({ className, data }: TotalSignedDocume
|
||||
<div className={className}>
|
||||
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">Total Completed Documents</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>Total Completed Documents</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@ -46,7 +52,7 @@ export const TotalSignedDocumentsChart = ({ className, data }: TotalSignedDocume
|
||||
fill="hsl(var(--primary))"
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={60}
|
||||
label="Total Completed Documents"
|
||||
label={_(msg`Total Completed Documents`)}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@ -4,6 +4,7 @@ import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -15,22 +16,26 @@ export const Typefully = ({ className, ...props }: TypefullyProps) => {
|
||||
<div className={className} {...props}>
|
||||
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">Twitter Stats</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>Twitter Stats</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="my-12 flex flex-col items-center gap-y-4 text-center">
|
||||
<FaXTwitter className="h-12 w-12" />
|
||||
<Link href="https://typefully.com/documenso/stats" target="_blank">
|
||||
<h1>Documenso on X</h1>
|
||||
<h1>
|
||||
<Trans>Documenso on X</Trans>
|
||||
</h1>
|
||||
</Link>
|
||||
<Button className="rounded-full" size="sm" asChild>
|
||||
<Link href="https://typefully.com/documenso/stats" target="_blank">
|
||||
View all stats
|
||||
<Trans>View all stats</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button className="rounded-full bg-white" size="sm" asChild>
|
||||
<Link href="https://twitter.com/documenso" target="_blank">
|
||||
Follow us on X
|
||||
<Trans>Follow us on X</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Caveat } from 'next/font/google';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import { Callout } from '~/components/(marketing)/callout';
|
||||
@ -25,6 +26,8 @@ const fontCaveat = Caveat({
|
||||
});
|
||||
|
||||
export default async function IndexPage() {
|
||||
setupI18nSSR();
|
||||
|
||||
const starCount = await fetch('https://api.github.com/repos/documenso/documenso', {
|
||||
headers: {
|
||||
accept: 'application/vnd.github.v3+json',
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@ -28,15 +31,21 @@ export type PricingPageProps = {
|
||||
};
|
||||
|
||||
export default function PricingPage() {
|
||||
setupI18nSSR();
|
||||
|
||||
return (
|
||||
<div className="mt-6 sm:mt-12">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1>
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">
|
||||
<Trans>Pricing</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-foreground mt-4 text-lg leading-normal">
|
||||
Designed for every stage of your journey.
|
||||
<Trans>Designed for every stage of your journey.</Trans>
|
||||
</p>
|
||||
<p className="text-foreground text-lg leading-normal">
|
||||
<Trans>Get started today.</Trans>
|
||||
</p>
|
||||
<p className="text-foreground text-lg leading-normal">Get started today.</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
@ -49,19 +58,21 @@ export default function PricingPage() {
|
||||
|
||||
<div className="mx-auto mt-36 max-w-2xl">
|
||||
<h2 className="text-center text-2xl font-semibold">
|
||||
None of these work for you? Try self-hosting!
|
||||
<Trans>None of these work for you? Try self-hosting!</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-center leading-relaxed">
|
||||
Our self-hosted option is great for small teams and individuals who need a simple
|
||||
solution. You can use our docker based setup to get started in minutes. Take control with
|
||||
full customizability and data ownership.
|
||||
<Trans>
|
||||
Our self-hosted option is great for small teams and individuals who need a simple
|
||||
solution. You can use our docker based setup to get started in minutes. Take control
|
||||
with full customizability and data ownership.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Button variant="outline" size="lg" className="rounded-full hover:cursor-pointer" asChild>
|
||||
<Link href="https://github.com/documenso/documenso" target="_blank" rel="noreferrer">
|
||||
Get Started
|
||||
<Trans>Get Started</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@ -75,120 +86,134 @@ export default function PricingPage() {
|
||||
<Accordion type="multiple" className="mt-8">
|
||||
<AccordionItem value="plan-differences">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
What is the difference between the plans?
|
||||
<Trans>What is the difference between the plans?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<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 will get access to all features we build this year, for no additional cost!
|
||||
Forever! Yes, that includes multiple users per account later. If you want Documenso
|
||||
for your enterprise, we are happy to talk about your needs.
|
||||
<Trans>
|
||||
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 will get access to all features we build this year, for no additional cost!
|
||||
Forever! Yes, that includes multiple users per account later. If you want Documenso
|
||||
for your enterprise, we are happy to talk about your needs.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="data-handling">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
How do you handle my data?
|
||||
<Trans>How do you handle my data?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<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.
|
||||
<Trans>
|
||||
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.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="should-use-cloud">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
Why should I use your hosting service?
|
||||
<Trans>Why should I use your hosting service?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<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
|
||||
trusted signing certificates which helps you to build trust with your customers.
|
||||
<Trans>
|
||||
Using our hosted version is the easiest way to get started, you can simply subscribe
|
||||
and start signing your documents. We take care of the infrastructure, so you can
|
||||
focus on your business. Additionally, when using our hosted version you benefit from
|
||||
our trusted signing certificates which helps you to build trust with your customers.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="how-to-contribute">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
How can I contribute?
|
||||
<Trans>How can I contribute?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<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"
|
||||
href="https://github.com/documenso/documenso/milestones"
|
||||
target="_blank"
|
||||
>
|
||||
Issues
|
||||
</Link>{' '}
|
||||
and join our{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
>
|
||||
Discord Community
|
||||
</Link>{' '}
|
||||
to keep up to date, on what the current priorities are. In any case, we are an open
|
||||
community and welcome all input, technical and non-technical ❤️
|
||||
<Trans>
|
||||
That's awesome. You can take a look at the current{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://github.com/documenso/documenso/milestones"
|
||||
target="_blank"
|
||||
>
|
||||
Issues
|
||||
</Link>{' '}
|
||||
and join our{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
>
|
||||
Discord Community
|
||||
</Link>{' '}
|
||||
to keep up to date, on what the current priorities are. In any case, we are an open
|
||||
community and welcome all input, technical and non-technical ❤️
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="can-i-use-documenso-commercially">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
Can I use Documenso commercially?
|
||||
<Trans>Can I use Documenso commercially?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<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.
|
||||
<Trans>
|
||||
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.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="why-prefer-documenso">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
Why should I prefer Documenso over DocuSign or some other signing tool?
|
||||
<Trans>Why should I prefer Documenso over DocuSign or some other signing tool?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<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.
|
||||
<Trans>
|
||||
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.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="where-can-i-get-support">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
Where can I get support?
|
||||
<Trans>Where can I get support?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<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"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="mailto:support@documenso.com"
|
||||
>
|
||||
support@documenso.com
|
||||
</Link>{' '}
|
||||
or{' '}
|
||||
<a
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
in our Discord-Support-Channel
|
||||
</a>{' '}
|
||||
please message either Lucas or Timur to get added to the channel if you are not
|
||||
already a member.
|
||||
<Trans>
|
||||
We are happy to assist you at{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="mailto:support@documenso.com"
|
||||
>
|
||||
support@documenso.com
|
||||
</Link>{' '}
|
||||
or{' '}
|
||||
<a
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
in our Discord-Support-Channel
|
||||
</a>{' '}
|
||||
please message either Lucas or Timur to get added to the channel if you are not
|
||||
already a member.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { Caveat, Inter } from 'next/font/google';
|
||||
import { cookies, headers } from 'next/headers';
|
||||
|
||||
import { AxiomWebVitals } from 'next-axiom';
|
||||
import { PublicEnvScript } from 'next-runtime-env';
|
||||
|
||||
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
|
||||
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
|
||||
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
|
||||
import { TrpcProvider } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -54,9 +59,29 @@ export function generateMetadata() {
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const flags = await getAllAnonymousFlags();
|
||||
|
||||
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined;
|
||||
|
||||
// Should be safe to remove when we upgrade NextJS.
|
||||
// https://github.com/vercel/next.js/pull/65008
|
||||
// Currently if the middleware sets the cookie, it's not accessible in the cookies
|
||||
// during the same render.
|
||||
// So we go the roundabout way of checking the header for the set-cookie value.
|
||||
if (!cookies().get('i18n')) {
|
||||
const setCookieValue = headers().get('set-cookie');
|
||||
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
|
||||
|
||||
if (i18nCookie) {
|
||||
const i18n = i18nCookie.split('=')[1];
|
||||
|
||||
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
|
||||
}
|
||||
}
|
||||
|
||||
const { lang, i18n } = setupI18nSSR(overrideLang);
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
lang={lang}
|
||||
className={cn(fontInter.variable, fontCaveat.variable)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
@ -65,6 +90,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="google" content="notranslate" />
|
||||
<PublicEnvScript />
|
||||
</head>
|
||||
|
||||
@ -78,7 +104,11 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<FeatureFlagProvider initialFlags={flags}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<PlausibleProvider>
|
||||
<TrpcProvider>{children}</TrpcProvider>
|
||||
<TrpcProvider>
|
||||
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}>
|
||||
{children}
|
||||
</I18nClientProvider>
|
||||
</TrpcProvider>
|
||||
</PlausibleProvider>
|
||||
</ThemeProvider>
|
||||
</FeatureFlagProvider>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@ -13,16 +15,20 @@ export const CallToAction = ({ className, utmSource = 'generic-cta' }: CallToAct
|
||||
return (
|
||||
<Card spotlight className={className}>
|
||||
<CardContent className="flex flex-col items-center justify-center p-12">
|
||||
<h2 className="text-center text-2xl font-bold">Join the Open Signing Movement</h2>
|
||||
<h2 className="text-center text-2xl font-bold">
|
||||
<Trans>Join the Open Signing Movement</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center leading-normal">
|
||||
Create your account and start using state-of-the-art document signing. Open and beautiful
|
||||
signing is within your grasp.
|
||||
<Trans>
|
||||
Create your account and start using state-of-the-art document signing. Open and
|
||||
beautiful signing is within your grasp.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="mt-8 rounded-full no-underline" size="lg" asChild>
|
||||
<Link href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=${utmSource}`} target="_blank">
|
||||
Get started
|
||||
<Trans>Get started</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { LuGithub } from 'react-icons/lu';
|
||||
|
||||
@ -15,23 +16,6 @@ export type CalloutProps = {
|
||||
export const Callout = ({ starCount }: CalloutProps) => {
|
||||
const event = usePlausible();
|
||||
|
||||
const onSignUpClick = () => {
|
||||
const el = document.getElementById('email');
|
||||
|
||||
if (el) {
|
||||
const { top } = el.getBoundingClientRect();
|
||||
|
||||
window.scrollTo({
|
||||
top: top - 120,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
el.focus();
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-4">
|
||||
<Link href="https://app.documenso.com/signup?utm_source=marketing-callout">
|
||||
@ -40,9 +24,9 @@ export const Callout = ({ starCount }: CalloutProps) => {
|
||||
variant="outline"
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
>
|
||||
Try our Free Plan
|
||||
<Trans>Try our Free Plan</Trans>
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
No Credit Card required
|
||||
<Trans>No Credit Card required</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { AutoplayType } from 'embla-carousel-autoplay';
|
||||
import Autoplay from 'embla-carousel-autoplay';
|
||||
import useEmblaCarousel from 'embla-carousel-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
@ -13,13 +16,13 @@ import { Slide } from './slide';
|
||||
|
||||
const SLIDES = [
|
||||
{
|
||||
label: 'Signing Process',
|
||||
label: msg`Signing Process`,
|
||||
type: 'video',
|
||||
srcLight: 'https://github.com/documenso/design/raw/main/marketing/signing.webm',
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/signing.webm',
|
||||
},
|
||||
{
|
||||
label: 'Teams',
|
||||
label: msg`Teams`,
|
||||
type: 'video',
|
||||
srcLight: 'https://github.com/documenso/design/raw/main/marketing/teams.webm',
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/teams.webm',
|
||||
@ -31,7 +34,7 @@ const SLIDES = [
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/zapier.webm',
|
||||
},
|
||||
{
|
||||
label: 'Direct Link',
|
||||
label: msg`Direct Link`,
|
||||
type: 'video',
|
||||
srcLight: 'https://github.com/documenso/design/raw/main/marketing/direct-links.webm',
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/direct-links.webm',
|
||||
@ -49,7 +52,7 @@ const SLIDES = [
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/api.webm',
|
||||
},
|
||||
{
|
||||
label: 'Profile',
|
||||
label: msg`Profile`,
|
||||
type: 'video',
|
||||
srcLight: 'https://github.com/documenso/design/raw/main/marketing/profile_teaser.webm',
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/profile_teaser.webm',
|
||||
@ -57,6 +60,8 @@ const SLIDES = [
|
||||
];
|
||||
|
||||
export const Carousel = () => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const slides = SLIDES;
|
||||
const [_isPlaying, setIsPlaying] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@ -73,6 +78,7 @@ export const Carousel = () => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }, [
|
||||
Autoplay({ playOnInit: true, delay: autoplayDelay[selectedIndex] || 5000 }),
|
||||
]);
|
||||
|
||||
const [emblaThumbsRef, emblaThumbsApi] = useEmblaCarousel(
|
||||
{
|
||||
loop: true,
|
||||
@ -84,19 +90,28 @@ export const Carousel = () => {
|
||||
|
||||
const onThumbClick = useCallback(
|
||||
(index: number) => {
|
||||
if (!emblaApi || !emblaThumbsApi) return;
|
||||
if (!emblaApi || !emblaThumbsApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
emblaApi.scrollTo(index);
|
||||
},
|
||||
[emblaApi, emblaThumbsApi],
|
||||
);
|
||||
|
||||
const onSelect = useCallback(() => {
|
||||
if (!emblaApi || !emblaThumbsApi) return;
|
||||
if (!emblaApi || !emblaThumbsApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedIndex(emblaApi.selectedScrollSnap());
|
||||
emblaThumbsApi.scrollTo(emblaApi.selectedScrollSnap());
|
||||
|
||||
resetProgress();
|
||||
const autoplay = emblaApi.plugins()?.autoplay;
|
||||
|
||||
// moduleResolution: bundler breaks this type
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const autoplay = emblaApi.plugins()?.autoplay as unknown as AutoplayType | undefined;
|
||||
|
||||
if (autoplay) {
|
||||
autoplay.reset();
|
||||
@ -167,11 +182,18 @@ export const Carousel = () => {
|
||||
}, [emblaApi, onSelect, mounted, resolvedTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
const autoplay = emblaApi?.plugins()?.autoplay;
|
||||
if (!autoplay) return;
|
||||
// moduleResolution: bundler breaks this type
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const autoplay = emblaApi?.plugins()?.autoplay as unknown as AutoplayType | undefined;
|
||||
|
||||
if (!autoplay || !emblaApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPlaying(autoplay.isPlaying());
|
||||
emblaApi
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(emblaApi as unknown as any)
|
||||
.on('autoplay:play', () => setIsPlaying(true))
|
||||
.on('autoplay:stop', () => setIsPlaying(false))
|
||||
.on('reInit', () => setIsPlaying(autoplay.isPlaying()));
|
||||
@ -233,7 +255,7 @@ export const Carousel = () => {
|
||||
src={resolvedTheme === 'dark' ? slide.srcDark : slide.srcLight}
|
||||
type="video/webm"
|
||||
/>
|
||||
Your browser does not support the video tag.
|
||||
<Trans>Your browser does not support the video tag.</Trans>
|
||||
</video>
|
||||
)}
|
||||
</div>
|
||||
@ -257,7 +279,7 @@ export const Carousel = () => {
|
||||
onClick={() => onThumbClick(index)}
|
||||
selected={index === selectedIndex}
|
||||
index={index}
|
||||
label={slide.label}
|
||||
label={typeof slide.label === 'string' ? slide.label : _(slide.label)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -12,13 +13,15 @@ export const Enterprise = () => {
|
||||
return (
|
||||
<div className="mx-auto mt-36 max-w-2xl">
|
||||
<h2 className="text-center text-2xl font-semibold">
|
||||
Enterprise Compliance, License or Technical Needs?
|
||||
<Trans>Enterprise Compliance, License or Technical Needs?</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-center leading-relaxed">
|
||||
Our Enterprise License is great large organizations looking to switch to Documenso for all
|
||||
their signing needs. It's availible for our cloud offering as well as self-hosted setups and
|
||||
offer a wide range of compliance and Adminstration Features.
|
||||
<Trans>
|
||||
Our Enterprise License is great for large organizations looking to switch to Documenso for
|
||||
all their signing needs. It's available for our cloud offering as well as self-hosted
|
||||
setups and offers a wide range of compliance and Adminstration Features.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
@ -28,7 +31,9 @@ export const Enterprise = () => {
|
||||
className="mt-6"
|
||||
onClick={() => event('enterprise-contact')}
|
||||
>
|
||||
<Button className="rounded-full text-base">Contact Us</Button>
|
||||
<Button className="rounded-full text-base">
|
||||
<Trans>Contact Us</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,8 @@ import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import cardBeautifulFigure from '@documenso/assets/images/card-beautiful-figure.png';
|
||||
import cardFastFigure from '@documenso/assets/images/card-fast-figure.png';
|
||||
@ -25,17 +27,23 @@ export const FasterSmarterBeautifulBento = ({
|
||||
/>
|
||||
</div>
|
||||
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
|
||||
A 10x better signing experience.
|
||||
<span className="block md:mt-0">Faster, smarter and more beautiful.</span>
|
||||
<Trans>A 10x better signing experience.</Trans>
|
||||
<span className="block md:mt-0">
|
||||
<Trans>Faster, smarter and more beautiful.</Trans>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
|
||||
<Card className="col-span-2" degrees={45} gradient>
|
||||
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
|
||||
<p className="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.
|
||||
<strong className="block">
|
||||
<Trans>Fast.</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
When it comes to sending or receiving a contract, you can count on lightning-fast
|
||||
speeds.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
|
||||
@ -51,9 +59,13 @@ export const FasterSmarterBeautifulBento = ({
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Beautiful.</strong>
|
||||
Because signing should be celebrated. That’s why we care about the smallest detail in
|
||||
our product.
|
||||
<strong className="block">
|
||||
<Trans>Beautiful.</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Because signing should be celebrated. That’s why we care about the smallest detail
|
||||
in our product.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
@ -69,8 +81,12 @@ export const FasterSmarterBeautifulBento = ({
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<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.
|
||||
<strong className="block">
|
||||
<Trans>Smart.</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Our custom templates come with smart rules that can help you save time and energy.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
|
||||
@ -5,6 +5,8 @@ import type { HTMLAttributes } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
import { LiaDiscord } from 'react-icons/lia';
|
||||
import { LuGithub } from 'react-icons/lu';
|
||||
@ -13,6 +15,8 @@ import LogoImage from '@documenso/assets/logo.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||
|
||||
import { I18nSwitcher } from '~/components/(marketing)/i18n-switcher';
|
||||
|
||||
// import { StatusWidgetContainer } from './status-widget-container';
|
||||
|
||||
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
||||
@ -24,22 +28,24 @@ const SOCIAL_LINKS = [
|
||||
];
|
||||
|
||||
const FOOTER_LINKS = [
|
||||
{ href: '/pricing', text: 'Pricing' },
|
||||
{ href: '/pricing', text: msg`Pricing` },
|
||||
{ href: '/singleplayer', text: 'Singleplayer' },
|
||||
{ href: 'https://docs.documenso.com', text: 'Documentation', target: '_blank' },
|
||||
{ href: 'mailto:support@documenso.com', text: 'Support', target: '_blank' },
|
||||
{ href: '/blog', text: 'Blog' },
|
||||
{ href: '/changelog', text: 'Changelog' },
|
||||
{ href: '/open', text: 'Open Startup' },
|
||||
{ href: '/design-system', text: 'Design' },
|
||||
{ href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' },
|
||||
{ href: 'https://status.documenso.com', text: 'Status', target: '_blank' },
|
||||
{ href: '/oss-friends', text: 'OSS Friends' },
|
||||
{ href: '/careers', text: 'Careers' },
|
||||
{ href: '/privacy', text: 'Privacy' },
|
||||
{ href: 'https://docs.documenso.com', text: msg`Documentation`, target: '_blank' },
|
||||
{ href: 'mailto:support@documenso.com', text: msg`Support`, target: '_blank' },
|
||||
{ href: '/blog', text: msg`Blog` },
|
||||
{ href: '/changelog', text: msg`Changelog` },
|
||||
{ href: '/open', text: msg`Open Startup` },
|
||||
{ href: '/design-system', text: msg`Design` },
|
||||
{ href: 'https://shop.documenso.com', text: msg`Shop`, target: '_blank' },
|
||||
{ href: 'https://status.documenso.com', text: msg`Status`, target: '_blank' },
|
||||
{ href: '/oss-friends', text: msg`OSS Friends` },
|
||||
{ href: '/careers', text: msg`Careers` },
|
||||
{ href: '/privacy', text: msg`Privacy` },
|
||||
];
|
||||
|
||||
export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
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">
|
||||
@ -80,7 +86,7 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
target={link.target}
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 break-words text-sm"
|
||||
>
|
||||
{link.text}
|
||||
{typeof link.text === 'string' ? link.text : _(link.text)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@ -90,8 +96,12 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
© {new Date().getFullYear()} Documenso, Inc. All rights reserved.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap">
|
||||
<ThemeSwitcher />
|
||||
<div className="flex flex-row-reverse items-center sm:flex-row">
|
||||
<I18nSwitcher className="text-muted-foreground ml-2 rounded-full font-normal sm:mr-2" />
|
||||
|
||||
<div className="flex flex-wrap">
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,8 +6,9 @@ import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import LogoImage from '@documenso/assets/logo.png';
|
||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
@ -19,10 +20,6 @@ export type HeaderProps = HTMLAttributes<HTMLElement>;
|
||||
export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||
|
||||
const { getFlag } = useFeatureFlags();
|
||||
|
||||
const isSinglePlayerModeMarketingEnabled = getFlag('marketing_header_single_player_mode');
|
||||
|
||||
return (
|
||||
<header className={cn('flex items-center justify-between', className)} {...props}>
|
||||
<div className="flex items-center space-x-4">
|
||||
@ -35,15 +32,6 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
height={25}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{isSinglePlayerModeMarketingEnabled && (
|
||||
<Link
|
||||
href="/singleplayer"
|
||||
className="bg-primary dark:text-background rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
|
||||
>
|
||||
Try now!
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center gap-x-6 md:flex">
|
||||
@ -51,7 +39,7 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
href="/pricing"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Pricing
|
||||
<Trans>Pricing</Trans>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
@ -59,21 +47,21 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
target="_blank"
|
||||
>
|
||||
Documentation
|
||||
<Trans>Documentation</Trans>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Blog
|
||||
<Trans>Blog</Trans>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/open"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Open Startup
|
||||
<Trans>Open Startup</Trans>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
@ -81,12 +69,12 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Sign in
|
||||
<Trans>Sign in</Trans>
|
||||
</Link>
|
||||
|
||||
<Button className="rounded-full" size="sm" asChild>
|
||||
<Link href="https://app.documenso.com/signup?utm_source=marketing-header" target="_blank">
|
||||
Sign up
|
||||
<Trans>Sign up</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import type { Variants } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
@ -96,8 +97,11 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
animate="animate"
|
||||
className="text-center text-4xl font-bold leading-tight tracking-tight md:text-[48px] lg:text-[64px]"
|
||||
>
|
||||
Document signing,
|
||||
<span className="block" /> finally open source.
|
||||
<Trans>
|
||||
Document signing,
|
||||
<span className="block" />
|
||||
finally open source.
|
||||
</Trans>
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
@ -112,39 +116,21 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
variant="outline"
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
>
|
||||
Try our Free Plan
|
||||
<Trans>Try our Free Plan</Trans>
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
No Credit Card required
|
||||
<Trans>No Credit Card required</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
|
||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
||||
<LuGithub className="mr-2 h-5 w-5" />
|
||||
Star on GitHub
|
||||
<Trans>Star on GitHub</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{match(heroMarketingCTA)
|
||||
.with('spm', () => (
|
||||
<motion.div
|
||||
variants={HeroTitleVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
className="border-primary bg-background hover:bg-muted mx-auto mt-8 w-60 rounded-xl border transition-colors duration-300"
|
||||
>
|
||||
<Link href="/singleplayer" className="block px-4 py-2 text-center">
|
||||
<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>
|
||||
))
|
||||
.with('productHunt', () => (
|
||||
<motion.div
|
||||
variants={HeroTitleVariants}
|
||||
|
||||
71
apps/marketing/src/components/(marketing)/i18n-switcher.tsx
Normal file
71
apps/marketing/src/components/(marketing)/i18n-switcher.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
import { LuLanguages } from 'react-icons/lu';
|
||||
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@documenso/ui/primitives/command';
|
||||
|
||||
type I18nSwitcherProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const I18nSwitcher = ({ className }: I18nSwitcherProps) => {
|
||||
const { i18n, _ } = useLingui();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [value, setValue] = useState(i18n.locale);
|
||||
|
||||
const setLanguage = async (lang: string) => {
|
||||
setValue(lang);
|
||||
setOpen(false);
|
||||
|
||||
await dynamicActivate(i18n, lang);
|
||||
await switchI18NLanguage(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button className={className} variant="ghost" onClick={() => setOpen(true)}>
|
||||
<LuLanguages className="mr-1.5 h-4 w-4" />
|
||||
{SUPPORTED_LANGUAGES[value]?.full || i18n.locale}
|
||||
</Button>
|
||||
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder={_(msg`Search languages...`)} />
|
||||
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
{Object.values(SUPPORTED_LANGUAGES).map((language) => (
|
||||
<CommandItem
|
||||
key={language.short}
|
||||
value={language.full}
|
||||
onSelect={async () => setLanguage(language.short)}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
value === language.short ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
{SUPPORTED_LANGUAGES[language.short].full}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -3,6 +3,8 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
import { LiaDiscord } from 'react-icons/lia';
|
||||
@ -19,11 +21,11 @@ export type MobileNavigationProps = {
|
||||
export const MENU_NAVIGATION_LINKS = [
|
||||
{
|
||||
href: '/pricing',
|
||||
text: 'Pricing',
|
||||
text: msg`Pricing`,
|
||||
},
|
||||
{
|
||||
href: 'https://documen.so/docs-nav',
|
||||
text: 'Documentation',
|
||||
text: msg`Documentation`,
|
||||
},
|
||||
{
|
||||
href: '/singleplayer',
|
||||
@ -31,36 +33,38 @@ export const MENU_NAVIGATION_LINKS = [
|
||||
},
|
||||
{
|
||||
href: '/blog',
|
||||
text: 'Blog',
|
||||
text: msg`Blog`,
|
||||
},
|
||||
{
|
||||
href: '/open',
|
||||
text: 'Open Startup',
|
||||
text: msg`Open Startup`,
|
||||
},
|
||||
{
|
||||
href: 'https://status.documenso.com',
|
||||
text: 'Status',
|
||||
text: msg`Status`,
|
||||
},
|
||||
{
|
||||
href: 'mailto:support@documenso.com',
|
||||
text: 'Support',
|
||||
text: msg`Support`,
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
href: '/privacy',
|
||||
text: 'Privacy',
|
||||
text: msg`Privacy`,
|
||||
},
|
||||
{
|
||||
href: 'https://app.documenso.com/signup?utm_source=marketing-header',
|
||||
text: 'Sign up',
|
||||
text: msg`Sign up`,
|
||||
},
|
||||
{
|
||||
href: 'https://app.documenso.com/signin?utm_source=marketing-header',
|
||||
text: 'Sign in',
|
||||
text: msg`Sign in`,
|
||||
},
|
||||
];
|
||||
|
||||
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const handleMenuItemClick = () => {
|
||||
@ -112,7 +116,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
||||
onClick={() => handleMenuItemClick()}
|
||||
target={target}
|
||||
>
|
||||
{text}
|
||||
{typeof text === 'string' ? text : _(text)}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
@ -2,6 +2,8 @@ import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import cardBuildFigure from '@documenso/assets/images/card-build-figure.png';
|
||||
import cardOpenFigure from '@documenso/assets/images/card-open-figure.png';
|
||||
@ -22,17 +24,23 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
|
||||
/>
|
||||
</div>
|
||||
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
|
||||
Truly your own.
|
||||
<span className="block md:mt-0">Customise and expand.</span>
|
||||
<Trans>Truly your own.</Trans>
|
||||
<span className="block md:mt-0">
|
||||
<Trans>Customise and expand.</Trans>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
|
||||
<Card className="col-span-2" degrees={45} gradient>
|
||||
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
|
||||
<p className="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
|
||||
<strong className="block">Open Source or Hosted.</strong>
|
||||
It’s up to you. Either clone our repository or rely on our easy to use hosting
|
||||
solution.
|
||||
<strong className="block">
|
||||
<Trans>Open Source or Hosted.</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
It’s up to you. Either clone our repository or rely on our easy to use hosting
|
||||
solution.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
|
||||
@ -48,8 +56,10 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Build on top.</strong>
|
||||
Make it your own through advanced customization and adjustability.
|
||||
<strong className="block">
|
||||
<Trans>Build on top.</Trans>
|
||||
</strong>
|
||||
<Trans>Make it your own through advanced customization and adjustability.</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
@ -65,9 +75,13 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<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.
|
||||
<strong className="block">
|
||||
<Trans>Template Store (Soon).</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Choose a template from the community app store. Or submit your own template for
|
||||
others to use.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
|
||||
@ -5,6 +5,7 @@ import { useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
|
||||
@ -36,7 +37,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
)}
|
||||
onClick={() => setPeriod('MONTHLY')}
|
||||
>
|
||||
Monthly
|
||||
<Trans>Monthly</Trans>
|
||||
{period === 'MONTHLY' && (
|
||||
<motion.div
|
||||
layoutId={SELECTED_PLAN_BAR_LAYOUT_ID}
|
||||
@ -56,9 +57,9 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
)}
|
||||
onClick={() => setPeriod('YEARLY')}
|
||||
>
|
||||
Yearly
|
||||
<Trans>Yearly</Trans>
|
||||
<div className="bg-muted text-foreground block rounded-full px-2 py-0.5 text-xs">
|
||||
Save $60 or $120
|
||||
<Trans>Save $60 or $120</Trans>
|
||||
</div>
|
||||
{period === 'YEARLY' && (
|
||||
<motion.div
|
||||
@ -75,11 +76,13 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
data-plan="free"
|
||||
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-foreground text-4xl font-medium">Free</p>
|
||||
<p className="text-foreground text-4xl font-medium">
|
||||
<Trans>Free</Trans>
|
||||
</p>
|
||||
<p className="text-primary mt-2.5 text-xl font-medium">$0</p>
|
||||
|
||||
<p className="text-foreground mt-4 max-w-[30ch] text-center">
|
||||
For small teams and individuals with basic needs.
|
||||
<Trans>For small teams and individuals with basic needs.</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="rounded-full text-base" asChild>
|
||||
@ -88,14 +91,20 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
target="_blank"
|
||||
className="mt-6"
|
||||
>
|
||||
Signup Now
|
||||
<Trans>Signup Now</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="mt-8 flex w-full flex-col divide-y">
|
||||
<p className="text-foreground py-4">5 standard documents per month</p>
|
||||
<p className="text-foreground py-4">Up to 10 recipients per document</p>
|
||||
<p className="text-foreground py-4">No credit card required</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>5 standard documents per month</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Up to 10 recipients per document</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>No credit card required</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
@ -105,7 +114,9 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
data-plan="individual"
|
||||
className="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-foreground text-4xl font-medium">Individual</p>
|
||||
<p className="text-foreground text-4xl font-medium">
|
||||
<Trans>Individual</Trans>
|
||||
</p>
|
||||
<div className="text-primary mt-2.5 text-xl font-medium">
|
||||
<AnimatePresence mode="wait">
|
||||
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
|
||||
@ -114,7 +125,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</div>
|
||||
|
||||
<p className="text-foreground mt-4 max-w-[30ch] text-center">
|
||||
Everything you need for a great signing experience.
|
||||
<Trans>Everything you need for a great signing experience.</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="mt-6 rounded-full text-base" asChild>
|
||||
@ -122,15 +133,23 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=pricing-individual-plan`}
|
||||
target="_blank"
|
||||
>
|
||||
Signup Now
|
||||
<Trans>Signup Now</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="mt-8 flex w-full flex-col divide-y">
|
||||
<p className="text-foreground py-4">Unlimited Documents per Month</p>
|
||||
<p className="text-foreground py-4">API Accesss</p>
|
||||
<p className="text-foreground py-4">Email and Discord Support</p>
|
||||
<p className="text-foreground py-4">Premium Profile Name</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Unlimited Documents per Month</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>API Access</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Email and Discord Support</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Premium Profile Name</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
@ -139,7 +158,9 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
data-plan="teams"
|
||||
className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border px-8 py-12 shadow-lg"
|
||||
>
|
||||
<p className="text-foreground text-4xl font-medium">Teams</p>
|
||||
<p className="text-foreground text-4xl font-medium">
|
||||
<Trans>Teams</Trans>
|
||||
</p>
|
||||
<div className="text-primary mt-2.5 text-xl font-medium">
|
||||
<AnimatePresence mode="wait">
|
||||
{period === 'MONTHLY' && <motion.div layoutId="pricingTeams">$50</motion.div>}
|
||||
@ -148,7 +169,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</div>
|
||||
|
||||
<p className="text-foreground mt-4 max-w-[30ch] text-center">
|
||||
For companies looking to scale across multiple teams.
|
||||
<Trans>For companies looking to scale across multiple teams.</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="mt-6 rounded-full text-base" asChild>
|
||||
@ -156,18 +177,28 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=pricing-teams-plan`}
|
||||
target="_blank"
|
||||
>
|
||||
Signup Now
|
||||
<Trans>Signup Now</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="mt-8 flex w-full flex-col divide-y">
|
||||
<p className="text-foreground py-4">Unlimited Documents per Month</p>
|
||||
<p className="text-foreground py-4">API Accesss</p>
|
||||
<p className="text-foreground py-4">Email and Discord Support</p>
|
||||
<p className="text-foreground py-4 font-medium">Team Inbox</p>
|
||||
<p className="text-foreground py-4">5 Users Included</p>
|
||||
<p className="text-foreground py-4">
|
||||
Add More Users for {period === 'MONTHLY' ? '$10/ mo.' : '$96/ yr.'}
|
||||
<Trans>Unlimited Documents per Month</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>API Access</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Email and Discord Support</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4 font-medium">
|
||||
<Trans>Team Inbox</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>5 Users Included</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Add More Users for {period === 'MONTHLY' ? '$10/ mo.' : '$96/ yr.'}</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,8 @@ import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import cardConnectionsFigure from '@documenso/assets/images/card-connections-figure.png';
|
||||
import cardPaidFigure from '@documenso/assets/images/card-paid-figure.png';
|
||||
@ -26,16 +28,20 @@ export const ShareConnectPaidWidgetBento = ({
|
||||
/>
|
||||
</div>
|
||||
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
|
||||
Integrates with all your favourite tools.
|
||||
<span className="block md:mt-0">Send, connect, receive and embed everywhere.</span>
|
||||
<Trans>Integrates with all your favourite tools.</Trans>
|
||||
<span className="block md:mt-0">
|
||||
<Trans>Send, connect, receive and embed everywhere.</Trans>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
|
||||
<Card className="col-span-2 lg:col-span-1" degrees={120} gradient>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Easy Sharing (Soon).</strong>
|
||||
Receive your personal link to share with everyone you care about.
|
||||
<strong className="block">
|
||||
<Trans>Easy Sharing (Soon).</Trans>
|
||||
</strong>
|
||||
<Trans>Receive your personal link to share with everyone you care about.</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
@ -51,9 +57,13 @@ export const ShareConnectPaidWidgetBento = ({
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Connections</strong>
|
||||
Create connections and automations with Zapier and more to integrate with your
|
||||
favorite tools.
|
||||
<strong className="block">
|
||||
<Trans>Connections</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Create connections and automations with Zapier and more to integrate with your
|
||||
favorite tools.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
@ -69,8 +79,12 @@ export const ShareConnectPaidWidgetBento = ({
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Get paid (Soon).</strong>
|
||||
Integrated payments with Stripe so you don’t have to worry about getting paid.
|
||||
<strong className="block">
|
||||
<Trans>Get paid (Soon).</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Integrated payments with Stripe so you don’t have to worry about getting paid.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
@ -86,9 +100,13 @@ export const ShareConnectPaidWidgetBento = ({
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<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.
|
||||
<strong className="block">
|
||||
<Trans>React Widget (Soon).</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Easily embed Documenso into your product. Simply copy and paste our react widget
|
||||
into your application.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
|
||||
39
apps/marketing/src/middleware.ts
Normal file
39
apps/marketing/src/middleware.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
|
||||
|
||||
export default function middleware(req: NextRequest) {
|
||||
const lang = extractSupportedLanguage({
|
||||
headers: req.headers,
|
||||
cookies: cookies(),
|
||||
});
|
||||
|
||||
const response = NextResponse.next();
|
||||
|
||||
response.cookies.set('i18n', lang);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - ingest (analytics)
|
||||
* - site.webmanifest
|
||||
*/
|
||||
{
|
||||
source: '/((?!api|_next/static|_next/image|ingest|favicon|site.webmanifest).*)',
|
||||
missing: [
|
||||
{ type: 'header', key: 'next-router-prefetch' },
|
||||
{ type: 'header', key: 'purpose', value: 'prefetch' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
22
apps/web/lingui.config.ts
Normal file
22
apps/web/lingui.config.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { LinguiConfig } from '@lingui/conf';
|
||||
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
|
||||
// Extends root lingui.config.cjs.
|
||||
const config: LinguiConfig = {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
locales: APP_I18N_OPTIONS.supportedLangs as unknown as string[],
|
||||
catalogs: [
|
||||
{
|
||||
path: '<rootDir>/../../packages/lib/translations/web/{locale}',
|
||||
include: ['<rootDir>/apps/web/src'],
|
||||
},
|
||||
{
|
||||
path: '<rootDir>/../../packages/lib/translations/{locale}/common',
|
||||
include: ['<rootDir>/packages/ui', '<rootDir>/packages/lib'],
|
||||
},
|
||||
],
|
||||
catalogsMergePath: '<rootDir>/../../packages/lib/translations/{locale}/web',
|
||||
};
|
||||
|
||||
export default config;
|
||||
@ -31,6 +31,7 @@ const config = {
|
||||
serverActions: {
|
||||
bodySizeLimit: '50mb',
|
||||
},
|
||||
swcPlugins: [['@lingui/swc-plugin', {}]],
|
||||
},
|
||||
reactStrictMode: true,
|
||||
transpilePackages: [
|
||||
@ -59,6 +60,13 @@ const config = {
|
||||
config.resolve.alias.canvas = false;
|
||||
}
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.po$/,
|
||||
use: {
|
||||
loader: '@lingui/loader',
|
||||
},
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
async rewrites() {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -11,7 +11,8 @@
|
||||
"e2e:prepare": "next build && next start",
|
||||
"lint:fix": "next lint --fix",
|
||||
"clean": "rimraf .next && rimraf node_modules",
|
||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs",
|
||||
"translate:compile": "lingui compile --typescript"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
@ -22,6 +23,8 @@
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@lingui/macro": "^4.11.1",
|
||||
"@lingui/react": "^4.11.1",
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"@simplewebauthn/server": "^9.0.3",
|
||||
"@tanstack/react-query": "^4.29.5",
|
||||
@ -57,6 +60,8 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/loader": "^4.11.1",
|
||||
"@lingui/swc-plugin": "4.0.6",
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@simplewebauthn/types": "^9.0.1",
|
||||
"@types/formidable": "^2.0.6",
|
||||
|
||||
2
apps/web/process-env.d.ts
vendored
2
apps/web/process-env.d.ts
vendored
@ -16,5 +16,7 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_OIDC_WELL_KNOWN: string;
|
||||
NEXT_PRIVATE_OIDC_CLIENT_ID: string;
|
||||
NEXT_PRIVATE_OIDC_CLIENT_SECRET: string;
|
||||
NEXT_PRIVATE_OIDC_ALLOW_SIGNUP?: string;
|
||||
NEXT_PRIVATE_OIDC_SKIP_VERIFY?: string;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/web/public/static/delete-team.png
Normal file
BIN
apps/web/public/static/delete-team.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
apps/web/public/static/delete-user.png
Normal file
BIN
apps/web/public/static/delete-user.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
@ -133,7 +133,12 @@ export const DocumentPageViewRecentActivity = ({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground dark:text-muted-foreground/70 flex-auto py-0.5 text-xs leading-5">
|
||||
<p
|
||||
className="text-muted-foreground dark:text-muted-foreground/70 flex-auto truncate py-0.5 text-xs leading-5"
|
||||
title={`${formatDocumentAuditLogAction(auditLog, userId).prefix} ${
|
||||
formatDocumentAuditLogAction(auditLog, userId).description
|
||||
}`}
|
||||
>
|
||||
<span className="text-foreground font-medium">
|
||||
{formatDocumentAuditLogAction(auditLog, userId).prefix}
|
||||
</span>{' '}
|
||||
|
||||
@ -110,7 +110,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
Documents
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-row justify-between truncate">
|
||||
<div>
|
||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||
{document.title}
|
||||
|
||||
@ -117,7 +117,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
||||
Document
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-col justify-between sm:flex-row">
|
||||
<div className="flex flex-col justify-between truncate sm:flex-row">
|
||||
<div>
|
||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||
{document.title}
|
||||
@ -139,7 +139,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
||||
documentStatus={document.status}
|
||||
/>
|
||||
|
||||
<DownloadAuditLogButton documentId={document.id} />
|
||||
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -9,10 +9,15 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DownloadAuditLogButtonProps = {
|
||||
className?: string;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
|
||||
export const DownloadAuditLogButton = ({
|
||||
className,
|
||||
teamId,
|
||||
documentId,
|
||||
}: DownloadAuditLogButtonProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: downloadAuditLogs, isLoading } =
|
||||
@ -20,7 +25,7 @@ export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditL
|
||||
|
||||
const onDownloadAuditLogsClick = async () => {
|
||||
try {
|
||||
const { url } = await downloadAuditLogs({ documentId });
|
||||
const { url } = await downloadAuditLogs({ teamId, documentId });
|
||||
|
||||
const iframe = Object.assign(document.createElement('iframe'), {
|
||||
src: url,
|
||||
|
||||
@ -4,6 +4,7 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
@ -41,6 +42,7 @@ export const DeleteDocumentDialog = ({
|
||||
const router = useRouter();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { refreshLimits } = useLimits();
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||
@ -48,6 +50,7 @@ export const DeleteDocumentDialog = ({
|
||||
const { mutateAsync: deleteDocument, isLoading } = trpcReact.document.deleteDocument.useMutation({
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
void refreshLimits();
|
||||
|
||||
toast({
|
||||
title: 'Document deleted',
|
||||
|
||||
@ -36,7 +36,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const { quota, remaining } = useLimits();
|
||||
const { quota, remaining, refreshLimits } = useLimits();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@ -71,6 +71,8 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
void refreshLimits();
|
||||
|
||||
toast({
|
||||
title: 'Document uploaded',
|
||||
description: 'Your document has been uploaded successfully.',
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DeclineTeamInvitationButtonProps = {
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const DeclineTeamInvitationButton = ({ teamId }: DeclineTeamInvitationButtonProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const {
|
||||
mutateAsync: declineTeamInvitation,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
} = trpc.team.declineTeamInvitation.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Declined team invitation',
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
description: 'Unable to decline this team invitation at this time.',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={async () => declineTeamInvitation({ teamId })}
|
||||
loading={isLoading}
|
||||
disabled={isLoading || isSuccess}
|
||||
variant="ghost"
|
||||
>
|
||||
Decline
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@ -19,6 +19,7 @@ import {
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
|
||||
import { AcceptTeamInvitationButton } from './accept-team-invitation-button';
|
||||
import { DeclineTeamInvitationButton } from './decline-team-invitation-button';
|
||||
|
||||
export const TeamInvitations = () => {
|
||||
const { data, isInitialLoading } = trpc.team.getTeamInvitations.useQuery();
|
||||
@ -68,7 +69,8 @@ export const TeamInvitations = () => {
|
||||
}
|
||||
secondaryText={formatTeamUrl(invitation.team.url)}
|
||||
rightSideComponent={
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto space-x-2">
|
||||
<DeclineTeamInvitationButton teamId={invitation.team.id} />
|
||||
<AcceptTeamInvitationButton teamId={invitation.team.id} />
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ export default async function ApiTokensPage() {
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<ApiTokenForm className="max-w-xl" />
|
||||
<ApiTokenForm className="max-w-xl" tokens={tokens} />
|
||||
|
||||
<hr className="mb-4 mt-8" />
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
@ -39,6 +39,7 @@ export const DirectTemplatePageView = ({
|
||||
directTemplateToken,
|
||||
}: TemplatesDirectPageViewProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -82,8 +83,15 @@ export const DirectTemplatePageView = ({
|
||||
|
||||
const onSignDirectTemplateSubmit = async (fields: DirectTemplateLocalField[]) => {
|
||||
try {
|
||||
let directTemplateExternalId = searchParams?.get('externalId') || undefined;
|
||||
|
||||
if (directTemplateExternalId) {
|
||||
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
||||
}
|
||||
|
||||
const token = await createDocumentFromDirectTemplate({
|
||||
directTemplateToken,
|
||||
directTemplateExternalId,
|
||||
directRecipientName: fullName,
|
||||
directRecipientEmail: recipient.email,
|
||||
templateUpdatedAt: template.updatedAt,
|
||||
|
||||
@ -41,6 +41,7 @@ import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
|
||||
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
|
||||
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
|
||||
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
|
||||
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
|
||||
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
@ -182,6 +183,15 @@ export const SignDirectTemplateForm = ({
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.INITIALS, () => (
|
||||
<InitialsField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<NameField
|
||||
key={field.id}
|
||||
|
||||
@ -13,6 +13,7 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
|
||||
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 { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
@ -77,6 +78,9 @@ export default async function CompletedSigningPage({
|
||||
}
|
||||
|
||||
const signatures = await getRecipientSignatures({ recipientId: recipient.id });
|
||||
const isExistingUser = await getUserByEmail({ email: recipient.email })
|
||||
.then((u) => !!u)
|
||||
.catch(() => false);
|
||||
|
||||
const recipientName =
|
||||
recipient.name ||
|
||||
@ -85,7 +89,7 @@ export default async function CompletedSigningPage({
|
||||
|
||||
const sessionData = await getServerSession();
|
||||
const isLoggedIn = !!sessionData?.user;
|
||||
const canSignUp = !isLoggedIn && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
||||
const canSignUp = !isExistingUser && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
140
apps/web/src/app/(signing)/sign/[token]/initials-field.tsx
Normal file
140
apps/web/src/app/(signing)/sign/[token]/initials-field.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
TSignFieldWithTokenMutationSchema,
|
||||
} from '@documenso/trpc/server/field-router/schema';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredSigningContext } from './provider';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
export type InitialsFieldProps = {
|
||||
field: FieldWithSignature;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const InitialsField = ({
|
||||
field,
|
||||
recipient,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: InitialsFieldProps) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { fullName } = useRequiredSigningContext();
|
||||
const initials = extractInitials(fullName);
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
|
||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
|
||||
const {
|
||||
mutateAsync: removeSignedFieldWithToken,
|
||||
isLoading: isRemoveSignedFieldWithTokenLoading,
|
||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
|
||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||
|
||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||
try {
|
||||
const value = initials ?? '';
|
||||
|
||||
const payload: TSignFieldWithTokenMutationSchema = {
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
value,
|
||||
isBase64: false,
|
||||
authOptions,
|
||||
};
|
||||
|
||||
if (onSignField) {
|
||||
await onSignField(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
await signFieldWithToken(payload);
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while signing the document.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRemove = async () => {
|
||||
try {
|
||||
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
};
|
||||
|
||||
if (onUnsignField) {
|
||||
await onUnsignField(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
await removeSignedFieldWithToken(payload);
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while removing the signature.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Initials">
|
||||
{isLoading && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{!field.inserted && (
|
||||
<p className="group-hover:text-primary text-muted-foreground duration-200 group-hover:text-yellow-300">
|
||||
Initials
|
||||
</p>
|
||||
)}
|
||||
|
||||
{field.inserted && (
|
||||
<p className="text-muted-foreground dark:text-background/80 truncate duration-200">
|
||||
{field.customText}
|
||||
</p>
|
||||
)}
|
||||
</SigningFieldContainer>
|
||||
);
|
||||
};
|
||||
@ -178,7 +178,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
Sign as {recipient.name}{' '}
|
||||
<span className="text-muted-foreground">({recipient.email})</span>
|
||||
<div className="text-muted-foreground">({recipient.email})</div>
|
||||
</DialogTitle>
|
||||
|
||||
<div>
|
||||
|
||||
@ -214,7 +214,7 @@ export const SignatureField = ({
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
Sign as {recipient.name}{' '}
|
||||
<span className="text-muted-foreground">({recipient.email})</span>
|
||||
<div className="text-muted-foreground h-5">({recipient.email})</div>
|
||||
</DialogTitle>
|
||||
|
||||
<div className="">
|
||||
|
||||
@ -39,7 +39,16 @@ export type SignatureFieldProps = {
|
||||
*/
|
||||
onSign?: (documentAuthValue?: TRecipientActionAuth) => Promise<void> | void;
|
||||
onRemove?: (fieldType?: string) => Promise<void> | void;
|
||||
type?: 'Date' | 'Email' | 'Name' | 'Signature' | 'Radio' | 'Dropdown' | 'Number' | 'Checkbox';
|
||||
type?:
|
||||
| 'Date'
|
||||
| 'Initials'
|
||||
| 'Email'
|
||||
| 'Name'
|
||||
| 'Signature'
|
||||
| 'Radio'
|
||||
| 'Dropdown'
|
||||
| 'Number'
|
||||
| 'Checkbox';
|
||||
tooltipText?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@ -20,13 +20,13 @@ import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
|
||||
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
|
||||
import { truncateTitle } from '~/helpers/truncate-title';
|
||||
|
||||
import { CheckboxField } from './checkbox-field';
|
||||
import { DateField } from './date-field';
|
||||
import { DropdownField } from './dropdown-field';
|
||||
import { EmailField } from './email-field';
|
||||
import { SigningForm } from './form';
|
||||
import { InitialsField } from './initials-field';
|
||||
import { NameField } from './name-field';
|
||||
import { NumberField } from './number-field';
|
||||
import { RadioField } from './radio-field';
|
||||
@ -46,24 +46,28 @@ export const SigningPageView = ({
|
||||
fields,
|
||||
completedFields,
|
||||
}: SigningPageViewProps) => {
|
||||
const truncatedTitle = truncateTitle(document.title);
|
||||
|
||||
const { documentData, documentMeta } = document;
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-screen-xl">
|
||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||
{truncatedTitle}
|
||||
{document.title}
|
||||
</h1>
|
||||
|
||||
<div className="mt-2.5 flex items-center gap-x-6">
|
||||
<p className="text-muted-foreground">
|
||||
{document.User.name} ({document.User.email}) has invited you to{' '}
|
||||
{recipient.role === RecipientRole.VIEWER && 'view'}
|
||||
{recipient.role === RecipientRole.SIGNER && 'sign'}
|
||||
{recipient.role === RecipientRole.APPROVER && 'approve'} this document.
|
||||
<p
|
||||
className="text-muted-foreground truncate"
|
||||
title={document.User.name ? document.User.name : ''}
|
||||
>
|
||||
{document.User.name}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
({document.User.email}) has invited you to{' '}
|
||||
{recipient.role === RecipientRole.VIEWER && 'view'}
|
||||
{recipient.role === RecipientRole.SIGNER && 'sign'}
|
||||
{recipient.role === RecipientRole.APPROVER && 'approve'} this document.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">
|
||||
<Card
|
||||
@ -98,6 +102,9 @@ export const SigningPageView = ({
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
<SignatureField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
.with(FieldType.INITIALS, () => (
|
||||
<InitialsField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<NameField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Edit, Loader, Mail, MoreHorizontal, X } from 'lucide-react';
|
||||
|
||||
import type { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
@ -14,6 +12,7 @@ import {
|
||||
} from '@documenso/ui/primitives/dropdown-menu';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { RemoveTeamEmailDialog } from '~/components/(teams)/dialogs/remove-team-email-dialog';
|
||||
import { UpdateTeamEmailDialog } from '~/components/(teams)/dialogs/update-team-email-dialog';
|
||||
|
||||
export type TeamsSettingsPageProps = {
|
||||
@ -21,8 +20,6 @@ export type TeamsSettingsPageProps = {
|
||||
};
|
||||
|
||||
export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: resendEmailVerification, isLoading: isResendingEmailVerification } =
|
||||
@ -44,56 +41,6 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteTeamEmail, isLoading: isDeletingTeamEmail } =
|
||||
trpc.team.deleteTeamEmail.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Team email has been removed',
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
description: 'Unable to remove team email at this time. Please try again.',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteTeamEmailVerification, isLoading: isDeletingTeamEmailVerification } =
|
||||
trpc.team.deleteTeamEmailVerification.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Email verification has been removed',
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
description: 'Unable to remove email verification at this time. Please try again.',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onRemove = async () => {
|
||||
if (team.teamEmail) {
|
||||
await deleteTeamEmail({ teamId: team.id });
|
||||
}
|
||||
|
||||
if (team.emailVerification) {
|
||||
await deleteTeamEmailVerification({ teamId: team.id });
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
@ -130,13 +77,16 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
disabled={isDeletingTeamEmail || isDeletingTeamEmailVerification}
|
||||
onClick={async () => onRemove()}
|
||||
>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Remove
|
||||
</DropdownMenuItem>
|
||||
<RemoveTeamEmailDialog
|
||||
team={team}
|
||||
teamName={team.name}
|
||||
trigger={
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Remove
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@ -26,7 +26,7 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||
|
||||
const team = await getTeamByUrl({ userId: user.id, teamUrl });
|
||||
|
||||
let tokens: GetTeamTokensResponse | null = null;
|
||||
let tokens: GetTeamTokensResponse | undefined = undefined;
|
||||
|
||||
try {
|
||||
tokens = await getTeamTokens({ userId: user.id, teamId: team.id });
|
||||
@ -63,7 +63,7 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<ApiTokenForm className="max-w-xl" teamId={team.id} />
|
||||
<ApiTokenForm className="max-w-xl" teamId={team.id} tokens={tokens} />
|
||||
|
||||
<hr className="mb-4 mt-8" />
|
||||
|
||||
|
||||
@ -4,7 +4,11 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||
import {
|
||||
IS_GOOGLE_SSO_ENABLED,
|
||||
IS_OIDC_SSO_ENABLED,
|
||||
OIDC_PROVIDER_LABEL,
|
||||
} from '@documenso/lib/constants/auth';
|
||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||
|
||||
import { SignInForm } from '~/components/forms/signin';
|
||||
@ -43,6 +47,7 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
|
||||
oidcProviderLabel={OIDC_PROVIDER_LABEL}
|
||||
/>
|
||||
|
||||
{NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
|
||||
|
||||
120
apps/web/src/app/(unauthenticated)/team/decline/[token]/page.tsx
Normal file
120
apps/web/src/app/(unauthenticated)/team/decline/[token]/page.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||
import { declineTeamInvitation } from '@documenso/lib/server-only/team/decline-team-invitation';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberInviteStatus } from '@documenso/prisma/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
type DeclineInvitationPageProps = {
|
||||
params: {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function DeclineInvitationPage({
|
||||
params: { token },
|
||||
}: DeclineInvitationPageProps) {
|
||||
const session = await getServerComponentSession();
|
||||
|
||||
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamMemberInvite) {
|
||||
return (
|
||||
<div className="w-screen max-w-lg px-4">
|
||||
<div className="w-full">
|
||||
<h1 className="text-4xl font-semibold">Invalid token</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||
This token is invalid or has expired. No action is needed.
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/">Return</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const team = await getTeamById({ teamId: teamMemberInvite.teamId });
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: {
|
||||
equals: teamMemberInvite.email,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
await declineTeamInvitation({ userId: user.id, teamId: team.id });
|
||||
}
|
||||
|
||||
if (!user && teamMemberInvite.status !== TeamMemberInviteStatus.DECLINED) {
|
||||
await prisma.teamMemberInvite.update({
|
||||
where: {
|
||||
id: teamMemberInvite.id,
|
||||
},
|
||||
data: {
|
||||
status: TeamMemberInviteStatus.DECLINED,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const email = encryptSecondaryData({
|
||||
data: teamMemberInvite.email,
|
||||
expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">Team invitation</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
You have been invited by <strong>{team.name}</strong> to join their team.
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-1 text-sm">
|
||||
To decline this invitation you must create an account.
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href={`/signup?email=${encodeURIComponent(email)}`}>Create account</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isSessionUserTheInvitedUser = user?.id === session.user?.id;
|
||||
|
||||
return (
|
||||
<div className="w-screen max-w-lg px-4">
|
||||
<h1 className="text-4xl font-semibold">Invitation declined</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||
You have declined the invitation from <strong>{team.name}</strong> to join their team.
|
||||
</p>
|
||||
|
||||
{isSessionUserTheInvitedUser ? (
|
||||
<Button asChild>
|
||||
<Link href="/">Return to Dashboard</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild>
|
||||
<Link href="/">Return to Home</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { type HTMLAttributes, useEffect, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { MenuIcon, SearchIcon } from 'lucide-react';
|
||||
|
||||
@ -41,6 +42,18 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const isPathTeamUrl = (teamUrl: string) => {
|
||||
if (!pathname || !pathname.startsWith(`/t/`)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pathname.split('/')[2] === teamUrl;
|
||||
};
|
||||
|
||||
const selectedTeam = teams?.find((team) => isPathTeamUrl(team.url));
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
@ -60,7 +73,10 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
|
||||
<DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
||||
|
||||
<div className="flex gap-x-4 md:ml-8">
|
||||
<div
|
||||
className="flex gap-x-4 md:ml-8"
|
||||
title={selectedTeam ? selectedTeam.name : user.name ?? ''}
|
||||
>
|
||||
<MenuSwitcher user={user} teams={teams} />
|
||||
</div>
|
||||
|
||||
|
||||
@ -113,10 +113,11 @@ export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialog
|
||||
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete team</DialogTitle>
|
||||
<DialogTitle>Are you sure you wish to delete this team?</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
Are you sure? This is irreversable.
|
||||
Please note that you will lose access to all documents associated with this team & all
|
||||
the members will be removed and notified
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@ -0,0 +1,153 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert } from '@documenso/ui/primitives/alert';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type RemoveTeamEmailDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
teamName: string;
|
||||
team: Prisma.TeamGetPayload<{
|
||||
include: {
|
||||
teamEmail: true;
|
||||
emailVerification: {
|
||||
select: {
|
||||
expiresAt: true;
|
||||
name: true;
|
||||
email: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEmailDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync: deleteTeamEmail, isLoading: isDeletingTeamEmail } =
|
||||
trpc.team.deleteTeamEmail.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Team email has been removed',
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
description: 'Unable to remove team email at this time. Please try again.',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteTeamEmailVerification, isLoading: isDeletingTeamEmailVerification } =
|
||||
trpc.team.deleteTeamEmailVerification.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Email verification has been removed',
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
description: 'Unable to remove email verification at this time. Please try again.',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onRemove = async () => {
|
||||
if (team.teamEmail) {
|
||||
await deleteTeamEmail({ teamId: team.id });
|
||||
}
|
||||
|
||||
if (team.emailVerification) {
|
||||
await deleteTeamEmailVerification({ teamId: team.id });
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => setOpen(value)}>
|
||||
<DialogTrigger asChild>
|
||||
{trigger ?? <Button variant="destructive">Remove team email</Button>}
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
You are about to delete the following team email from{' '}
|
||||
<span className="font-semibold">{teamName}</span>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Alert variant="neutral" padding="tight">
|
||||
<AvatarWithText
|
||||
avatarClass="h-12 w-12"
|
||||
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
|
||||
avatarFallback={extractInitials(
|
||||
(team.teamEmail?.name || team.emailVerification?.name) ?? '',
|
||||
)}
|
||||
primaryText={
|
||||
<span className="text-foreground/80 text-sm font-semibold">
|
||||
{team.teamEmail?.name || team.emailVerification?.name}
|
||||
</span>
|
||||
}
|
||||
secondaryText={
|
||||
<span className="text-sm">
|
||||
{team.teamEmail?.email || team.emailVerification?.email}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</Alert>
|
||||
|
||||
<fieldset disabled={isDeletingTeamEmail || isDeletingTeamEmailVerification}>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
loading={isDeletingTeamEmail || isDeletingTeamEmailVerification}
|
||||
onClick={async () => onRemove()}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -98,6 +98,7 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl
|
||||
{
|
||||
type: P.union(
|
||||
FieldType.NAME,
|
||||
FieldType.INITIALS,
|
||||
FieldType.EMAIL,
|
||||
FieldType.NUMBER,
|
||||
FieldType.RADIO,
|
||||
|
||||
@ -71,6 +71,7 @@ export type SignInFormProps = {
|
||||
initialEmail?: string;
|
||||
isGoogleSSOEnabled?: boolean;
|
||||
isOIDCSSOEnabled?: boolean;
|
||||
oidcProviderLabel?: string;
|
||||
};
|
||||
|
||||
export const SignInForm = ({
|
||||
@ -78,6 +79,7 @@ export const SignInForm = ({
|
||||
initialEmail,
|
||||
isGoogleSSOEnabled,
|
||||
isOIDCSSOEnabled,
|
||||
oidcProviderLabel,
|
||||
}: SignInFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const { getFlag } = useFeatureFlags();
|
||||
@ -369,7 +371,7 @@ export const SignInForm = ({
|
||||
onClick={onSignInWithOIDCClick}
|
||||
>
|
||||
<FaIdCardClip className="mr-2 h-5 w-5" />
|
||||
OIDC
|
||||
{oidcProviderLabel || 'OIDC'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||
import type { ApiToken } from '@documenso/prisma/client';
|
||||
import { TRPCClientError } from '@documenso/trpc/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
||||
@ -44,23 +46,37 @@ const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
|
||||
|
||||
type TCreateTokenFormSchema = z.infer<typeof ZCreateTokenFormSchema>;
|
||||
|
||||
type NewlyCreatedToken = {
|
||||
id: number;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type ApiTokenFormProps = {
|
||||
className?: string;
|
||||
teamId?: number;
|
||||
tokens?: Pick<ApiToken, 'id'>[];
|
||||
};
|
||||
|
||||
export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
|
||||
export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
|
||||
const router = useRouter();
|
||||
const [isTransitionPending, startTransition] = useTransition();
|
||||
|
||||
const [, copy] = useCopyToClipboard();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [newlyCreatedToken, setNewlyCreatedToken] = useState('');
|
||||
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
|
||||
const [noExpirationDate, setNoExpirationDate] = useState(false);
|
||||
|
||||
// This lets us hide the token from being copied if it has been deleted without
|
||||
// resorting to a useEffect or any other fanciness. This comes at the cost of it
|
||||
// taking slighly longer to appear since it will need to wait for the router.refresh()
|
||||
// to finish updating.
|
||||
const hasNewlyCreatedToken =
|
||||
tokens?.find((token) => token.id === newlyCreatedToken?.id) !== undefined;
|
||||
|
||||
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
|
||||
onSuccess(data) {
|
||||
setNewlyCreatedToken(data.token);
|
||||
setNewlyCreatedToken(data);
|
||||
},
|
||||
});
|
||||
|
||||
@ -110,7 +126,7 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
|
||||
|
||||
form.reset();
|
||||
|
||||
router.refresh();
|
||||
startTransition(() => router.refresh());
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
@ -216,7 +232,7 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
|
||||
type="submit"
|
||||
className="hidden md:inline-flex"
|
||||
disabled={!form.formState.isDirty}
|
||||
loading={form.formState.isSubmitting}
|
||||
loading={form.formState.isSubmitting || isTransitionPending}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
@ -225,7 +241,7 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!form.formState.isDirty}
|
||||
loading={form.formState.isSubmitting}
|
||||
loading={form.formState.isSubmitting || isTransitionPending}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
@ -234,24 +250,33 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{newlyCreatedToken && (
|
||||
<Card className="mt-8" gradient>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
Your token was created successfully! Make sure to copy it because you won't be able to
|
||||
see it again!
|
||||
</p>
|
||||
<AnimatePresence initial={!hasNewlyCreatedToken}>
|
||||
{newlyCreatedToken && hasNewlyCreatedToken && (
|
||||
<motion.div
|
||||
className="mt-8"
|
||||
initial={{ opacity: 0, y: -40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 40 }}
|
||||
>
|
||||
<Card gradient>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
Your token was created successfully! Make sure to copy it because you won't be
|
||||
able to see it again!
|
||||
</p>
|
||||
|
||||
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
|
||||
{newlyCreatedToken}
|
||||
</p>
|
||||
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
|
||||
{newlyCreatedToken.token}
|
||||
</p>
|
||||
|
||||
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken)}>
|
||||
Copy token
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
|
||||
Copy token
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -60,13 +60,23 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
});
|
||||
},
|
||||
linkAccount: async ({ user }) => {
|
||||
linkAccount: async ({ user, account, profile }) => {
|
||||
const userId = typeof user.id === 'string' ? parseInt(user.id) : user.id;
|
||||
|
||||
if (isNaN(userId)) {
|
||||
if (Number.isNaN(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user is linking an OIDC account and the email verified date is set then update it in the db.
|
||||
if (account.provider === 'oidc' && profile.emailVerified !== null) {
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
emailVerified: profile.emailVerified,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
|
||||
8
crowdin.yml
Normal file
8
crowdin.yml
Normal file
@ -0,0 +1,8 @@
|
||||
# Any changes to these paths should be reflected in the root lingui.config.ts
|
||||
files:
|
||||
- source: packages/lib/translations/en/common.po
|
||||
translation: packages/lib/translations/%two_letters_code%/%original_file_name%
|
||||
- source: packages/lib/translations/en/marketing.po
|
||||
translation: packages/lib/translations/%two_letters_code%/%original_file_name%
|
||||
- source: packages/lib/translations/en/web.po
|
||||
translation: packages/lib/translations/%two_letters_code%/%original_file_name%
|
||||
28
lingui.config.ts
Normal file
28
lingui.config.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { LinguiConfig } from '@lingui/conf';
|
||||
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
|
||||
const config: LinguiConfig = {
|
||||
sourceLocale: APP_I18N_OPTIONS.sourceLang,
|
||||
locales: APP_I18N_OPTIONS.supportedLangs as unknown as string[],
|
||||
// Any changes to these catalogue paths should be reflected in crowdin.yml
|
||||
catalogs: [
|
||||
{
|
||||
path: '<rootDir>/packages/lib/translations/{locale}/marketing',
|
||||
include: ['apps/marketing/src'],
|
||||
exclude: ['**/node_modules/**'],
|
||||
},
|
||||
{
|
||||
path: '<rootDir>/packages/lib/translations/{locale}/web',
|
||||
include: ['apps/web/src'],
|
||||
exclude: ['**/node_modules/**'],
|
||||
},
|
||||
{
|
||||
path: '<rootDir>/packages/lib/translations/{locale}/common',
|
||||
include: ['packages/ui', 'packages/lib'],
|
||||
exclude: ['**/node_modules/**'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
2658
package-lock.json
generated
2658
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
@ -31,7 +31,9 @@
|
||||
"precommit": "npm install && git add package.json package-lock.json",
|
||||
"trigger:dev": "npm run with:env -- npx trigger-cli dev --handler-path=\"/api/jobs\"",
|
||||
"inngest:dev": "inngest dev -u http://localhost:3000/api/jobs",
|
||||
"make:version": " npm version --workspace @documenso/web --workspace @documenso/marketing --include-workspace-root --no-git-tag-version -m \"v%s\""
|
||||
"make:version": " npm version --workspace @documenso/web --workspace @documenso/marketing --include-workspace-root --no-git-tag-version -m \"v%s\"",
|
||||
"translate:extract": "lingui extract",
|
||||
"translate:compile": "turbo run translate:compile --filter=@documenso/web --filter=@documenso/marketing --filter=@documenso/ui"
|
||||
},
|
||||
"packageManager": "npm@10.7.0",
|
||||
"engines": {
|
||||
@ -41,6 +43,7 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.7.1",
|
||||
"@commitlint/config-conventional": "^17.7.0",
|
||||
"@lingui/cli": "^4.11.1",
|
||||
"@trigger.dev/cli": "^2.3.18",
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
@ -60,6 +63,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@documenso/pdf-sign": "^0.1.0",
|
||||
"@lingui/core": "^4.11.1",
|
||||
"inngest-cli": "^0.29.1",
|
||||
"next-runtime-env": "^3.2.0",
|
||||
"react": "18.2.0"
|
||||
|
||||
@ -2,6 +2,9 @@ import { createNextRoute } from '@ts-rest/next';
|
||||
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import '@documenso/lib/constants/time-zones';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
||||
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||
@ -222,6 +225,36 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
const dateFormat = body.meta.dateFormat
|
||||
? DATE_FORMATS.find((format) => format.label === body.meta.dateFormat)
|
||||
: DATE_FORMATS.find((format) => format.value === DEFAULT_DOCUMENT_DATE_FORMAT);
|
||||
const timezone = body.meta.timezone
|
||||
? TIME_ZONES.find((tz) => tz === body.meta.timezone)
|
||||
: DEFAULT_DOCUMENT_TIME_ZONE;
|
||||
|
||||
const isDateFormatValid = body.meta.dateFormat
|
||||
? DATE_FORMATS.some((format) => format.label === dateFormat?.label)
|
||||
: true;
|
||||
const isTimeZoneValid = body.meta.timezone ? TIME_ZONES.includes(String(timezone)) : true;
|
||||
|
||||
if (!isDateFormatValid) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Invalid date format. Please provide a valid date format',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!isTimeZoneValid) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Invalid timezone. Please provide a valid timezone',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
|
||||
|
||||
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
|
||||
@ -244,7 +277,11 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
...body.meta,
|
||||
subject: body.meta.subject,
|
||||
message: body.meta.message,
|
||||
timezone,
|
||||
dateFormat: dateFormat?.value,
|
||||
redirectUrl: body.meta.redirectUrl,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import { extendZodWithOpenApi } from '@anatine/zod-openapi';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import '@documenso/lib/constants/time-zones';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { ZUrlSchema } from '@documenso/lib/schemas/common';
|
||||
import {
|
||||
DocumentDataType,
|
||||
@ -11,6 +15,8 @@ import {
|
||||
TemplateType,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZNoBodyMutationSchema = null;
|
||||
|
||||
/**
|
||||
@ -97,8 +103,19 @@ export const ZCreateDocumentMutationSchema = z.object({
|
||||
.object({
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
timezone: z.string(),
|
||||
dateFormat: z.string(),
|
||||
timezone: z.string().default(DEFAULT_DOCUMENT_TIME_ZONE).openapi({
|
||||
description:
|
||||
'The timezone of the date. Must be one of the options listed in the list below.',
|
||||
enum: TIME_ZONES,
|
||||
}),
|
||||
dateFormat: z
|
||||
.string()
|
||||
.default(DEFAULT_DOCUMENT_DATE_FORMAT)
|
||||
.openapi({
|
||||
description:
|
||||
'The format of the date. Must be one of the options listed in the list below.',
|
||||
enum: DATE_FORMATS.map((format) => format.value),
|
||||
}),
|
||||
redirectUrl: z.string(),
|
||||
})
|
||||
.partial(),
|
||||
|
||||
@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
|
||||
import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedPendingDocument } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
@ -31,8 +31,6 @@ test('[DOCUMENT_AUTH]: should grant access when not required', async ({ page })
|
||||
await page.goto(`/sign/${token}`);
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
}
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_AUTH]: should allow or deny access when required', async ({ page }) => {
|
||||
@ -90,7 +88,4 @@ test('[DOCUMENT_AUTH]: should allow or deny access when required', async ({ page
|
||||
await expect(page.getByRole('paragraph')).toContainText(email);
|
||||
}
|
||||
}
|
||||
|
||||
await unseedUser(user.id);
|
||||
await unseedUser(recipientWithAccount.id);
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
seedPendingDocumentNoFields,
|
||||
seedPendingDocumentWithFullFields,
|
||||
} from '@documenso/prisma/seed/documents';
|
||||
import { seedTestEmail, seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
||||
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||
|
||||
@ -60,9 +60,6 @@ test('[DOCUMENT_AUTH]: should allow signing when no auth setup', async ({ page }
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
}
|
||||
|
||||
await unseedUser(user.id);
|
||||
await unseedUser(recipientWithAccount.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_AUTH]: should allow signing with valid global auth', async ({ page }) => {
|
||||
@ -119,9 +116,6 @@ test('[DOCUMENT_AUTH]: should allow signing with valid global auth', async ({ pa
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
|
||||
await unseedUser(user.id);
|
||||
await unseedUser(recipientWithAccount.id);
|
||||
});
|
||||
|
||||
// Currently document auth for signing/approving/viewing is not required.
|
||||
@ -154,9 +148,6 @@ test.skip('[DOCUMENT_AUTH]: should deny signing document when required for globa
|
||||
await expect(page.getByRole('paragraph')).toContainText(
|
||||
'Reauthentication is required to sign the document',
|
||||
);
|
||||
|
||||
await unseedUser(user.id);
|
||||
await unseedUser(recipientWithAccount.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth', async ({
|
||||
@ -196,9 +187,6 @@ test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth'
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
}
|
||||
}
|
||||
|
||||
await unseedUser(user.id);
|
||||
await unseedUser(recipientWithAccount.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_AUTH]: should allow field signing when required for recipient auth', async ({
|
||||
|
||||
@ -6,8 +6,8 @@ import {
|
||||
seedPendingDocument,
|
||||
} from '@documenso/prisma/seed/documents';
|
||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
||||
import { seedTeam, unseedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
@ -53,8 +53,6 @@ test.describe('[EE_ONLY]', () => {
|
||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW] enterprise team member can add action auth settings', async ({ page }) => {
|
||||
@ -94,8 +92,6 @@ test.describe('[EE_ONLY]', () => {
|
||||
|
||||
// Advanced settings should be visible.
|
||||
await expect(page.getByLabel('Show advanced settings')).toBeVisible();
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW] enterprise team member should not have access to enterprise on personal account', async ({
|
||||
@ -130,8 +126,6 @@ test.describe('[EE_ONLY]', () => {
|
||||
|
||||
// Advanced settings should not be visible.
|
||||
await expect(page.getByLabel('Show advanced settings')).not.toBeVisible();
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
});
|
||||
|
||||
@ -166,8 +160,6 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
||||
|
||||
await expect(page.getByLabel('Title')).toHaveValue('New Title');
|
||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: title should be disabled depending on document status', async ({ page }) => {
|
||||
@ -188,6 +180,4 @@ test('[DOCUMENT_FLOW]: title should be disabled depending on document status', a
|
||||
// Should be enabled for draft documents.
|
||||
await page.goto(`/documents/${draftDocument.id}/edit`);
|
||||
await expect(page.getByLabel('Title')).toBeEnabled();
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
||||
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
@ -57,8 +57,6 @@ test.describe('[EE_ONLY]', () => {
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
// Todo: Fix stepper component back issue before finishing test.
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
});
|
||||
|
||||
@ -91,6 +89,4 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
seedBlankDocument,
|
||||
seedPendingDocumentWithFullFields,
|
||||
} from '@documenso/prisma/seed/documents';
|
||||
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
@ -107,8 +107,6 @@ test('[DOCUMENT_FLOW]: should be able to create a document', async ({ page }) =>
|
||||
|
||||
// Assert document was created
|
||||
await expect(page.getByRole('link', { name: documentTitle })).toBeVisible();
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipients', async ({
|
||||
@ -192,8 +190,6 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
|
||||
|
||||
// Assert document was created
|
||||
await expect(page.getByRole('link', { name: documentTitle })).toBeVisible();
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipients with different roles', async ({
|
||||
@ -291,8 +287,6 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
|
||||
|
||||
// Assert document was created
|
||||
await expect(page.getByRole('link', { name: 'Test Title' })).toBeVisible();
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: should not be able to create a document without signatures', async ({
|
||||
@ -331,8 +325,6 @@ test('[DOCUMENT_FLOW]: should not be able to create a document without signature
|
||||
await expect(
|
||||
page.getByRole('dialog').getByText('No signature field found').first(),
|
||||
).toBeVisible();
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) => {
|
||||
@ -388,8 +380,6 @@ test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) =
|
||||
.click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
}
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a document and redirect to redirect url', async ({
|
||||
@ -462,8 +452,6 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
|
||||
// Check if document has been signed
|
||||
const { status: completedStatus } = await getDocumentByToken(token);
|
||||
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', async ({ page }) => {
|
||||
@ -505,6 +493,4 @@ test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', asyn
|
||||
// Check if document has been signed
|
||||
const { status: completedStatus } = await getDocumentByToken(token);
|
||||
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
|
||||
|
||||
await unseedUser(user.id);
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { test } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { seedTeam, unseedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
@ -33,8 +33,6 @@ test('[TEAMS]: create team', async ({ page }) => {
|
||||
|
||||
// Goto new team settings page.
|
||||
await page.getByRole('row').filter({ hasText: teamId }).getByRole('link').nth(1).click();
|
||||
|
||||
await unseedTeam(teamId);
|
||||
});
|
||||
|
||||
test('[TEAMS]: delete team', async ({ page }) => {
|
||||
@ -84,6 +82,4 @@ test('[TEAMS]: update team', async ({ page }) => {
|
||||
|
||||
// Check we have been redirected to the new team URL and the name is updated.
|
||||
await page.waitForURL(`${WEBAPP_BASE_URL}/t/${updatedTeamId}/settings`);
|
||||
|
||||
await unseedTeam(updatedTeamId);
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
|
||||
import { seedTeamEmail, unseedTeam, unseedTeamEmail } from '@documenso/prisma/seed/teams';
|
||||
import { seedTeamEmail } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||
@ -42,8 +42,6 @@ test('[TEAMS]: check team documents count', async ({ page }) => {
|
||||
|
||||
await apiSignout({ page });
|
||||
}
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: check team documents count with internal team email', async ({ page }) => {
|
||||
@ -138,9 +136,6 @@ test('[TEAMS]: check team documents count with internal team email', async ({ pa
|
||||
|
||||
await apiSignout({ page });
|
||||
}
|
||||
|
||||
await unseedTeamEmail({ teamId: team.id });
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: check team documents count with external team email', async ({ page }) => {
|
||||
@ -225,9 +220,6 @@ test('[TEAMS]: check team documents count with external team email', async ({ pa
|
||||
await checkDocumentTabCount(page, 'Completed', 0);
|
||||
await checkDocumentTabCount(page, 'Draft', 1);
|
||||
await checkDocumentTabCount(page, 'All', 3);
|
||||
|
||||
await unseedTeamEmail({ teamId: team.id });
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: resend pending team document', async ({ page }) => {
|
||||
@ -284,8 +276,6 @@ test('[TEAMS]: delete draft team document', async ({ page }) => {
|
||||
|
||||
await apiSignout({ page });
|
||||
}
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: delete pending team document', async ({ page }) => {
|
||||
@ -325,8 +315,6 @@ test('[TEAMS]: delete pending team document', async ({ page }) => {
|
||||
|
||||
await apiSignout({ page });
|
||||
}
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: delete completed team document', async ({ page }) => {
|
||||
@ -366,6 +354,4 @@ test('[TEAMS]: delete completed team document', async ({ page }) => {
|
||||
|
||||
await apiSignout({ page });
|
||||
}
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { seedTeam, seedTeamEmailVerification, unseedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
||||
import { seedTeam, seedTeamEmailVerification } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
@ -31,8 +31,6 @@ test('[TEAMS]: send team email request', async ({ page }) => {
|
||||
.filter({ hasText: 'We have sent a confirmation email for verification.' })
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: accept team email request', async ({ page }) => {
|
||||
@ -41,14 +39,12 @@ test('[TEAMS]: accept team email request', async ({ page }) => {
|
||||
});
|
||||
|
||||
const teamEmailVerification = await seedTeamEmailVerification({
|
||||
email: 'team-email-verification@test.documenso.com',
|
||||
email: `team-email-verification--${team.url}@test.documenso.com`,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await page.goto(`${WEBAPP_BASE_URL}/team/verify/email/${teamEmailVerification.token}`);
|
||||
await expect(page.getByRole('heading')).toContainText('Team email verified!');
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: delete team email', async ({ page }) => {
|
||||
@ -66,10 +62,9 @@ test('[TEAMS]: delete team email', async ({ page }) => {
|
||||
await page.locator('section div').filter({ hasText: 'Team email' }).getByRole('button').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||
await page.getByRole('button', { name: 'Remove' }).click();
|
||||
|
||||
await expect(page.getByText('Team email has been removed').first()).toBeVisible();
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: team email owner removes access', async ({ page }) => {
|
||||
@ -96,7 +91,4 @@ test('[TEAMS]: team email owner removes access', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Revoke' }).click();
|
||||
|
||||
await expect(page.getByText('You have successfully revoked').first()).toBeVisible();
|
||||
|
||||
await unseedTeam(team.url);
|
||||
await unseedUser(teamEmailOwner.id);
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { seedTeam, seedTeamInvite, unseedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedTeam, seedTeamInvite } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
@ -35,8 +35,6 @@ test('[TEAMS]: update team member role', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('row').filter({ hasText: teamMemberToUpdate.user.email }),
|
||||
).toContainText('Manager');
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: accept team invitation without account', async ({ page }) => {
|
||||
@ -49,8 +47,6 @@ test('[TEAMS]: accept team invitation without account', async ({ page }) => {
|
||||
|
||||
await page.goto(`${WEBAPP_BASE_URL}/team/invite/${teamInvite.token}`);
|
||||
await expect(page.getByRole('heading')).toContainText('Team invitation');
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: accept team invitation with account', async ({ page }) => {
|
||||
@ -64,8 +60,6 @@ test('[TEAMS]: accept team invitation with account', async ({ page }) => {
|
||||
|
||||
await page.goto(`${WEBAPP_BASE_URL}/team/invite/${teamInvite.token}`);
|
||||
await expect(page.getByRole('heading')).toContainText('Invitation accepted!');
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: member can leave team', async ({ page }) => {
|
||||
@ -88,8 +82,6 @@ test('[TEAMS]: member can leave team', async ({ page }) => {
|
||||
await expect(page.getByRole('status').first()).toContainText(
|
||||
'You have successfully left this team.',
|
||||
);
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: owner cannot leave team', async ({ page }) => {
|
||||
@ -105,6 +97,4 @@ test('[TEAMS]: owner cannot leave team', async ({ page }) => {
|
||||
});
|
||||
|
||||
await expect(page.getByRole('button').getByText('Leave')).toBeDisabled();
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { seedTeam, seedTeamTransfer, unseedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedTeam, seedTeamTransfer } from '@documenso/prisma/seed/teams';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
@ -43,8 +43,6 @@ test('[TEAMS]: initiate and cancel team transfer', async ({ page }) => {
|
||||
await expect(page.getByRole('status').first()).toContainText(
|
||||
'The team transfer invitation has been successfully deleted.',
|
||||
);
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
/**
|
||||
@ -64,6 +62,4 @@ test.skip('[TEAMS]: accept team transfer', async ({ page }) => {
|
||||
|
||||
await page.goto(`${WEBAPP_BASE_URL}/team/verify/transfer/${teamTransferRequest.token}`);
|
||||
await expect(page.getByRole('heading')).toContainText('Team ownership transferred!');
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user