mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Compare commits
22 Commits
feat/open-
...
feat-early
| Author | SHA1 | Date | |
|---|---|---|---|
| 833584bd4f | |||
| ca52a22bcd | |||
| ba611b9adb | |||
| e8643850a0 | |||
| bec7376fe8 | |||
| 7363bbc8f7 | |||
| d0f679e177 | |||
| e9769904ab | |||
| a2746742a7 | |||
| 459e3dc45b | |||
| 323f1974ab | |||
| 2bfc2b0c1f | |||
| f003a2864e | |||
| 6e2d1fb148 | |||
| 81f9b2f776 | |||
| 334671ef85 | |||
| e5fa2f4490 | |||
| 0732aa317d | |||
| 190ae18edc | |||
| 82ffbeeb21 | |||
| 9affd7b7fa | |||
| 897ba629df |
67
apps/marketing/content/blog/early-adopters.mdx
Normal file
67
apps/marketing/content/blog/early-adopters.mdx
Normal file
@ -0,0 +1,67 @@
|
||||
---
|
||||
title: The Early Adopters Plan
|
||||
description: Launch Week Day 4 and we are still early! Early enough for you to get a sweet deal for supporting Documenso's Mission. Join the movement and get a shiny early adopter account in the process.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2023-09-28
|
||||
tags:
|
||||
- Paid Plan
|
||||
- Metrics
|
||||
- Open Startup
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/early.png"
|
||||
width="1260"
|
||||
height="630"
|
||||
alt="XKCD: Bug"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
"Being early is, uh, good." -Unknown
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
> TLDR; we have 100 early adopter accounts available at a great price. [Secure yours now](https://documenso.com/pricing).
|
||||
|
||||
## Community-Driven Development
|
||||
|
||||
As we ramp up hiring and development speed for Documenso, I want to discuss how we plan to build its core version.
|
||||
As we want to be a highly community-driven product, we want to capture the thoughts and ideas of said community in two ways:
|
||||
|
||||
1. Everything we plan (i.e. documen.so/roadmap) and build (i.e. documen.so/repo) is public and open to comments and suggestions from everyone by design. We plan to have discussions, streams, and articles showcasing what we're up to, encouraging feedback and iterations on our plans.
|
||||
2. We're looking for 100 die-hard early adopters to get more deep hands-on feedback. If you want to be among the first to use and help shape Documenso, we have a special offer for you:
|
||||
|
||||
## The Early Adopter Plan
|
||||
|
||||
- All first 100 signups are entitled to the early adopter plan.
|
||||
- The plan includes everything we build in the next 12 months and unlimited<sup>1</sup> signatures
|
||||
- This plan is priced at $30/mo.
|
||||
- No matter what we add, the price is guaranteed for life as a thank you for supporting Documenso's mission<sup>2</sup>. And we plan to add a lot.
|
||||
- This also includes unlimited users<sup>3</sup> as part of the upcoming support for teams.
|
||||
- If you already claimed an early adopter account in the past and canceled, we are happy to reactivate your account with the early adopter pricing. Reach out to support@documenso.com
|
||||
|
||||
## Being an Early Adopter
|
||||
|
||||
Being eligible for the early adopter plan has no formal requirements like giving feedback or being active in the community. In good faith, we assume you sign the Documenso Supporter Pledge to be part of this. And if you want to use the newest version quietly, that's fine, too. Everything the die-hard community brings up will be carefully considered and prioritized <sup>4</sup>. Also, there will obviously be limited edition merch available for the first 100.
|
||||
Documenso currently runs the community reviewed 0.9.1 version. Getting from here to the globally loved and adopted signing tool we all deserve will take a lot of work, and we want you on board to help us create it. Join us in shaping the future of open signing and having fun doing it. [Malfunction Mania](https://documenso.com/blog/malfunction-mania) and releasing 1.0 will go a long way.
|
||||
|
||||
## Extending our open metrics
|
||||
|
||||
As part of our ongoing effort to be open and transparent in our doing, we are adding "Early Adopters" to our [/open page](https://documenso.com/open) page. After we exceed the early adopter slots, this metric will transition to "Customers". When no more early adopter seats can be claimed, the early adopter plan will transition to a standard paid plan. It will still be priced at $30/mo., but will no longer include upcoming features or unlimited seats. You can [claim your early adopter account here](https://documenso.com/pricing).
|
||||
|
||||
If you have any questions or comments, please reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord).
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
|
||||
\
|
||||
[1] Excluding signatures, we have to pay for i.e. third-party QES
|
||||
|
||||
[2] The Documenso Manifest
|
||||
|
||||
[3] Within reason. If you are unsure what that means, feel free to contact hi@documenso.com. But it should be fine if you don't plan to onboard a huge enterprise.
|
||||
|
||||
[4] We won't be able to build everything everyone asks for. But we firmly plan to listen and build in a way everyone's requirements are met as well as possible.
|
||||
BIN
apps/marketing/public/blog/early.png
Normal file
BIN
apps/marketing/public/blog/early.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
@ -48,9 +48,9 @@ export default function PricingPage() {
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
You can self-host Documenso for free or use our ready-to-use hosted version. The
|
||||
hosted version comes with additional support, painless scalability and more. Early
|
||||
adopters of the community plan will get access to all features we build this year, for
|
||||
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.
|
||||
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.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Get the Community Plan
|
||||
Get the Early Adopters Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
$30/mo. forever!
|
||||
</span>
|
||||
|
||||
@ -139,7 +139,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
||||
</div>
|
||||
|
||||
<Button type="submit" size="lg" loading={isSubmitting}>
|
||||
Claim the Community Plan (
|
||||
Claim the early adopters Plan (
|
||||
{/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
|
||||
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||
? 'Monthly'
|
||||
|
||||
@ -114,7 +114,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Get the Community Plan
|
||||
Get the Early Adopters Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
$30/mo. forever!
|
||||
</span>
|
||||
|
||||
@ -119,7 +119,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
data-plan="community"
|
||||
className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border-2 px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380]"
|
||||
>
|
||||
<p className="text-foreground text-4xl font-medium">Community</p>
|
||||
<p className="text-foreground text-4xl font-medium">Early Adopters</p>
|
||||
<div className="text-primary mt-2.5 text-xl font-medium">
|
||||
<AnimatePresence mode="wait">
|
||||
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
|
||||
@ -136,12 +136,22 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</ClaimPlanDialog>
|
||||
|
||||
<div className="mt-8 flex w-full flex-col divide-y">
|
||||
<p className="text-foreground py-4 font-medium">Documenso Early Adopter Deal:</p>
|
||||
<p className="text-foreground py-4 font-medium">
|
||||
{' '}
|
||||
<a href="https://documenso.com/blog/early-adopters" target="_blank">
|
||||
The Early Adopter Deal:
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-foreground py-4">Join the movement</p>
|
||||
<p className="text-foreground py-4">Simple signing solution</p>
|
||||
<p className="text-foreground py-4">Email and Slack assistance</p>
|
||||
<p className="text-foreground py-4">Email, Discord and Slack assistance</p>
|
||||
<p className="text-foreground py-4">
|
||||
<strong>Includes all upcoming features</strong>
|
||||
<strong>
|
||||
{' '}
|
||||
<a href="https://documenso.com/blog/early-adopters" target="_blank">
|
||||
Includes all upcoming features
|
||||
</a>
|
||||
</strong>
|
||||
</p>
|
||||
<p className="text-foreground py-4">Fixed, straightforward pricing</p>
|
||||
</div>
|
||||
@ -168,7 +178,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</Link>
|
||||
|
||||
<div className="mt-8 flex w-full flex-col divide-y">
|
||||
<p className="text-foreground py-4 font-medium">Everything in Community, plus:</p>
|
||||
<p className="text-foreground py-4 font-medium">Everything in Early Adopters, plus:</p>
|
||||
<p className="text-foreground py-4">Custom Subdomain</p>
|
||||
<p className="text-foreground py-4">Compliance Check</p>
|
||||
<p className="text-foreground py-4">Guaranteed Uptime</p>
|
||||
|
||||
@ -177,9 +177,7 @@ export const createSinglePlayerDocument = async (
|
||||
},
|
||||
);
|
||||
|
||||
// Todo: Handle `downloadLink`
|
||||
const template = createElement(DocumentSelfSignedEmailTemplate, {
|
||||
downloadLink: `${process.env.NEXT_PUBLIC_MARKETING_URL}/single-player-mode/${documentToken}`,
|
||||
documentName: documentName,
|
||||
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
|
||||
});
|
||||
@ -197,6 +195,7 @@ export const createSinglePlayerDocument = async (
|
||||
subject: 'Document signed',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
attachments: [{ content: Buffer.from(pdfBytes), filename: documentName }],
|
||||
});
|
||||
|
||||
return documentToken;
|
||||
|
||||
@ -189,7 +189,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
||||
className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<h3 className="text-2xl font-semibold">Sign up for the community plan</h3>
|
||||
<h3 className="text-2xl font-semibold">Sign up for the early adopters plan</h3>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
with Timur Ercan & Lucas Smith from Documenso
|
||||
</p>
|
||||
|
||||
BIN
apps/web/public/static/user-plus.png
Normal file
BIN
apps/web/public/static/user-plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 367 B |
BIN
packages/email/static/user-plus.png
Normal file
BIN
packages/email/static/user-plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 367 B |
@ -1,7 +1,9 @@
|
||||
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
|
||||
import { Button, Column, Img, Section, Tailwind, Text } from '@react-email/components';
|
||||
|
||||
import * as config from '@documenso/tailwind-config';
|
||||
|
||||
import { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export interface TemplateDocumentCompletedProps {
|
||||
downloadLink: string;
|
||||
documentName: string;
|
||||
@ -27,27 +29,20 @@ export const TemplateDocumentCompleted = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section>
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Column>
|
||||
<Img
|
||||
className="h-42 mx-auto"
|
||||
src={getAssetUrl('/static/document.png')}
|
||||
alt="Documenso"
|
||||
/>
|
||||
<Section>
|
||||
<Section className="mb-4">
|
||||
<Column align="center">
|
||||
<Text className="text-base font-semibold text-[#7AC455]">
|
||||
<Img
|
||||
src={getAssetUrl('/static/completed.png')}
|
||||
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||
/>
|
||||
Completed
|
||||
</Text>
|
||||
</Column>
|
||||
|
||||
<Column />
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
|
||||
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||
Completed
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||
“{documentName}” was signed by all signers
|
||||
@ -66,10 +61,13 @@ export const TemplateDocumentCompleted = ({
|
||||
Review
|
||||
</Button> */}
|
||||
<Button
|
||||
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||
className="rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||
href={downloadLink}
|
||||
>
|
||||
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" />
|
||||
<Img
|
||||
src={getAssetUrl('/static/download.png')}
|
||||
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
|
||||
/>
|
||||
Download
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
import { Column, Img, Row, Section } from '@react-email/components';
|
||||
|
||||
export interface TemplateDocumentImageProps {
|
||||
assetBaseUrl: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TemplateDocumentImage = ({ assetBaseUrl, className }: TemplateDocumentImageProps) => {
|
||||
const getAssetUrl = (path: string) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Section className={className}>
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
|
||||
<Column>
|
||||
<Img className="h-42 mx-auto" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
</Column>
|
||||
|
||||
<Column />
|
||||
</Row>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateDocumentImage;
|
||||
@ -1,7 +1,9 @@
|
||||
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
|
||||
import { Button, Section, Tailwind, Text } from '@react-email/components';
|
||||
|
||||
import * as config from '@documenso/tailwind-config';
|
||||
|
||||
import { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export interface TemplateDocumentInviteProps {
|
||||
inviterName: string;
|
||||
inviterEmail: string;
|
||||
@ -16,10 +18,6 @@ export const TemplateDocumentInvite = ({
|
||||
signDocumentLink,
|
||||
assetBaseUrl,
|
||||
}: TemplateDocumentInviteProps) => {
|
||||
const getAssetUrl = (path: string) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tailwind
|
||||
config={{
|
||||
@ -30,21 +28,7 @@ export const TemplateDocumentInvite = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section className="mt-4">
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
|
||||
<Column>
|
||||
<Img
|
||||
className="h-42 mx-auto"
|
||||
src={getAssetUrl('/static/document.png')}
|
||||
alt="Documenso"
|
||||
/>
|
||||
</Column>
|
||||
|
||||
<Column />
|
||||
</Row>
|
||||
</Section>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Section>
|
||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
|
||||
import { Column, Img, Section, Tailwind, Text } from '@react-email/components';
|
||||
|
||||
import * as config from '@documenso/tailwind-config';
|
||||
|
||||
import { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export interface TemplateDocumentPendingProps {
|
||||
documentName: string;
|
||||
assetBaseUrl: string;
|
||||
@ -25,27 +27,20 @@ export const TemplateDocumentPending = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section>
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Column>
|
||||
<Img
|
||||
className="h-42 mx-auto"
|
||||
src={getAssetUrl('/static/document.png')}
|
||||
alt="Documenso"
|
||||
/>
|
||||
<Section>
|
||||
<Section className="mb-4">
|
||||
<Column align="center">
|
||||
<Text className="text-base font-semibold text-blue-500">
|
||||
<Img
|
||||
src={getAssetUrl('/static/clock.png')}
|
||||
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||
/>
|
||||
Waiting for others
|
||||
</Text>
|
||||
</Column>
|
||||
|
||||
<Column />
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500">
|
||||
<Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||
Waiting for others
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||
“{documentName}” has been signed
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
|
||||
import { Button, Column, Img, Link, Section, Tailwind, Text } from '@react-email/components';
|
||||
|
||||
import * as config from '@documenso/tailwind-config';
|
||||
|
||||
import { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export interface TemplateDocumentSelfSignedProps {
|
||||
downloadLink: string;
|
||||
documentName: string;
|
||||
assetBaseUrl: string;
|
||||
}
|
||||
|
||||
export const TemplateDocumentSelfSigned = ({
|
||||
downloadLink,
|
||||
documentName,
|
||||
assetBaseUrl,
|
||||
}: TemplateDocumentSelfSignedProps) => {
|
||||
const signUpUrl = `${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`;
|
||||
|
||||
const getAssetUrl = (path: string) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
};
|
||||
@ -27,39 +29,59 @@ export const TemplateDocumentSelfSigned = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Section className="flex-row items-center justify-center">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
</div>
|
||||
<Section>
|
||||
<Column align="center">
|
||||
<Text className="text-base font-semibold text-[#7AC455]">
|
||||
<Img
|
||||
src={getAssetUrl('/static/completed.png')}
|
||||
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||
/>
|
||||
Completed
|
||||
</Text>
|
||||
</Column>
|
||||
</Section>
|
||||
|
||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
|
||||
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||
Completed
|
||||
</Text>
|
||||
|
||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||
<Text className="text-primary mb-0 mt-6 text-center text-lg font-semibold">
|
||||
You have signed “{documentName}”
|
||||
</Text>
|
||||
|
||||
<Text className="my-1 text-center text-base text-slate-400">
|
||||
Check out our plans to access the full suite of features.
|
||||
<Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
|
||||
Create a{' '}
|
||||
<Link
|
||||
href={signUpUrl}
|
||||
target="_blank"
|
||||
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
|
||||
>
|
||||
free account
|
||||
</Link>{' '}
|
||||
to access your signed documents at any time.
|
||||
</Text>
|
||||
|
||||
<Section className="mb-6 mt-8 text-center">
|
||||
<Button
|
||||
className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||
href={signUpUrl}
|
||||
className="mr-4 rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||
>
|
||||
<Img
|
||||
src={getAssetUrl('/static/user-plus.png')}
|
||||
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
|
||||
/>
|
||||
Create account
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||
href="https://documenso.com/pricing"
|
||||
>
|
||||
<Img src={getAssetUrl('/static/review.png')} className="-mb-1 mr-2 inline h-5 w-5" />
|
||||
<Img
|
||||
src={getAssetUrl('/static/review.png')}
|
||||
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
|
||||
/>
|
||||
View plans
|
||||
</Button>
|
||||
<Button
|
||||
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||
href={downloadLink}
|
||||
>
|
||||
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" />
|
||||
Download
|
||||
</Button>
|
||||
</Section>
|
||||
</Section>
|
||||
</Tailwind>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
|
||||
import { Button, Section, Tailwind, Text } from '@react-email/components';
|
||||
|
||||
import * as config from '@documenso/tailwind-config';
|
||||
|
||||
import { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export type TemplateForgotPasswordProps = {
|
||||
resetPasswordLink: string;
|
||||
assetBaseUrl: string;
|
||||
@ -11,10 +13,6 @@ export const TemplateForgotPassword = ({
|
||||
resetPasswordLink,
|
||||
assetBaseUrl,
|
||||
}: TemplateForgotPasswordProps) => {
|
||||
const getAssetUrl = (path: string) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tailwind
|
||||
config={{
|
||||
@ -25,11 +23,9 @@ export const TemplateForgotPassword = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section className="mt-4 flex-row items-center justify-center">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
</div>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Section className="flex-row items-center justify-center">
|
||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||
Forgot your password?
|
||||
</Text>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { Img, Section, Tailwind, Text } from '@react-email/components';
|
||||
import { Button, Section, Tailwind, Text } from '@react-email/components';
|
||||
|
||||
import * as config from '@documenso/tailwind-config';
|
||||
|
||||
import { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export interface TemplateResetPasswordProps {
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
@ -9,10 +11,6 @@ export interface TemplateResetPasswordProps {
|
||||
}
|
||||
|
||||
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
|
||||
const getAssetUrl = (path: string) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tailwind
|
||||
config={{
|
||||
@ -23,11 +21,9 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section className="mt-4 flex-row items-center justify-center">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
</div>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Section className="flex-row items-center justify-center">
|
||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||
Password updated!
|
||||
</Text>
|
||||
@ -35,6 +31,15 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
|
||||
<Text className="my-1 text-center text-base text-slate-400">
|
||||
Your password has been updated.
|
||||
</Text>
|
||||
|
||||
<Section className="mb-6 mt-8 text-center">
|
||||
<Button
|
||||
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
|
||||
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signin`}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
</Section>
|
||||
</Section>
|
||||
</Tailwind>
|
||||
);
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
TemplateDocumentCompleted,
|
||||
TemplateDocumentCompletedProps,
|
||||
} from '../template-components/template-document-completed';
|
||||
import TemplateFooter from '../template-components/template-footer';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
|
||||
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
TemplateDocumentInvite,
|
||||
TemplateDocumentInviteProps,
|
||||
} from '../template-components/template-document-invite';
|
||||
import TemplateFooter from '../template-components/template-footer';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
|
||||
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
||||
customBody?: string;
|
||||
|
||||
@ -15,12 +15,11 @@ import {
|
||||
TemplateDocumentSelfSigned,
|
||||
TemplateDocumentSelfSignedProps,
|
||||
} from '../template-components/template-document-self-signed';
|
||||
import TemplateFooter from '../template-components/template-footer';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
|
||||
export type DocumentSelfSignedTemplateProps = TemplateDocumentSelfSignedProps;
|
||||
|
||||
export const DocumentSelfSignedEmailTemplate = ({
|
||||
downloadLink = 'https://documenso.com',
|
||||
documentName = 'Open Source Pledge.pdf',
|
||||
assetBaseUrl = 'http://localhost:3002',
|
||||
}: DocumentSelfSignedTemplateProps) => {
|
||||
@ -54,7 +53,6 @@ export const DocumentSelfSignedEmailTemplate = ({
|
||||
/>
|
||||
|
||||
<TemplateDocumentSelfSigned
|
||||
downloadLink={downloadLink}
|
||||
documentName={documentName}
|
||||
assetBaseUrl={assetBaseUrl}
|
||||
/>
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
|
||||
import config from '@documenso/tailwind-config';
|
||||
|
||||
import TemplateFooter from '../template-components/template-footer';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
import {
|
||||
TemplateForgotPassword,
|
||||
TemplateForgotPasswordProps,
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
|
||||
import config from '@documenso/tailwind-config';
|
||||
|
||||
import TemplateFooter from '../template-components/template-footer';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
import {
|
||||
TemplateResetPassword,
|
||||
TemplateResetPasswordProps,
|
||||
|
||||
@ -351,7 +351,11 @@ export const AddFieldsFormPartial = ({
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput />
|
||||
<CommandEmpty />
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
No recipient matching this description was found.
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{recipients.map((recipient, index) => (
|
||||
|
||||
@ -126,12 +126,12 @@ export const AddSignersFormPartial = ({
|
||||
<AnimatePresence>
|
||||
{signers.map((signer, index) => (
|
||||
<motion.div
|
||||
key={signer.formId}
|
||||
key={signer.id}
|
||||
data-native-id={signer.nativeId}
|
||||
className="flex flex-wrap items-end gap-x-4"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={`signer-${signer.formId}-email`}>
|
||||
<Label htmlFor={`signer-${signer.id}-email`}>
|
||||
Email
|
||||
<span className="text-destructive ml-1 inline-block font-medium">*</span>
|
||||
</Label>
|
||||
@ -141,7 +141,7 @@ export const AddSignersFormPartial = ({
|
||||
name={`signers.${index}.email`}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id={`signer-${signer.formId}-email`}
|
||||
id={`signer-${signer.id}-email`}
|
||||
type="email"
|
||||
className="bg-background mt-2"
|
||||
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
|
||||
@ -153,14 +153,14 @@ export const AddSignersFormPartial = ({
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={`signer-${signer.formId}-name`}>Name</Label>
|
||||
<Label htmlFor={`signer-${signer.id}-name`}>Name</Label>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name={`signers.${index}.name`}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id={`signer-${signer.formId}-name`}
|
||||
id={`signer-${signer.id}-name`}
|
||||
type="text"
|
||||
className="bg-background mt-2"
|
||||
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
|
||||
@ -195,7 +195,11 @@ export const AddSignersFormPartial = ({
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<FormErrorMessage className="mt-2" error={errors.signers} />
|
||||
<FormErrorMessage
|
||||
className="mt-2"
|
||||
// Dirty hack to handle errors when .root is populated for an array type
|
||||
error={'signers__root' in errors && errors['signers__root']}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<Button type="button" disabled={isSubmitting} onClick={() => onAddSigner()}>
|
||||
|
||||
@ -1,19 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZAddSignersFormSchema = z.object({
|
||||
signers: z
|
||||
.array(
|
||||
export const ZAddSignersFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().min(1).email(),
|
||||
name: z.string(),
|
||||
}),
|
||||
)
|
||||
.refine((signers) => {
|
||||
const emails = signers.map((signer) => signer.email);
|
||||
),
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
}, 'Signers must have unique emails'),
|
||||
});
|
||||
},
|
||||
// Dirty hack to handle errors when .root is populated for an array type
|
||||
{ message: 'Signers must have unique emails', path: ['signers__root'] },
|
||||
);
|
||||
|
||||
export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;
|
||||
|
||||
@ -4,13 +4,17 @@ import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
export type FormErrorMessageProps = {
|
||||
className?: string;
|
||||
error: { message?: string } | undefined;
|
||||
error: { message?: string } | undefined | unknown;
|
||||
};
|
||||
|
||||
const isErrorWithMessage = (error: unknown): error is { message?: string } => {
|
||||
return typeof error === 'object' && error !== null && 'message' in error;
|
||||
};
|
||||
|
||||
export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) => {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
{isErrorWithMessage(error) && (
|
||||
<motion.p
|
||||
initial={{
|
||||
opacity: 0,
|
||||
|
||||
Reference in New Issue
Block a user