Compare commits
4 Commits
feat/open-
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e2d1fb148 | |||
| 81f9b2f776 | |||
| 334671ef85 | |||
| e5fa2f4490 |
@ -50,9 +50,7 @@ NEXT_PRIVATE_SMTP_SECURE=
|
|||||||
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
|
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
|
||||||
# REQUIRED: Defines the email address to use as the from address.
|
# REQUIRED: Defines the email address to use as the from address.
|
||||||
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
|
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
|
||||||
# OPTIONAL: The API key to use for Resend.com
|
# OPTIONAL: The API key to use for the MailChannels proxy endpoint.
|
||||||
NEXT_PRIVATE_RESEND_API_KEY=
|
|
||||||
# OPTIONAL: The API key to use for MailChannels.
|
|
||||||
NEXT_PRIVATE_MAILCHANNELS_API_KEY=
|
NEXT_PRIVATE_MAILCHANNELS_API_KEY=
|
||||||
# OPTIONAL: The endpoint to use for the MailChannels API if using a proxy.
|
# OPTIONAL: The endpoint to use for the MailChannels API if using a proxy.
|
||||||
NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=
|
NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=
|
||||||
|
|||||||
@ -51,7 +51,4 @@ As Documenso 1.0 just hit the staging environment, we're calling a MALFUNCTION M
|
|||||||
|
|
||||||
We don't have a specific end date for Malfunction Mania. We plan to move the staging version into the production environment by the end of the month once we're happy with the results. Bug reports and fixes are, of course, always welcome going forward.
|
We don't have a specific end date for Malfunction Mania. We plan to move the staging version into the production environment by the end of the month once we're happy with the results. Bug reports and fixes are, of course, always welcome going forward.
|
||||||
|
|
||||||
Best from Hamburg
|
|
||||||
Timur
|
|
||||||
|
|
||||||
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.**
|
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.**
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: Merch Mania
|
title: Shop and Limited Edition "Mania" Shirt
|
||||||
description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can.
|
description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can.
|
||||||
authorName: 'Timur Ercan'
|
authorName: 'Timur Ercan'
|
||||||
authorImage: '/blog/blog-author-timur.jpeg'
|
authorImage: '/blog/blog-author-timur.jpeg'
|
||||||
authorRole: 'Co-Founder'
|
authorRole: 'Co-Founder'
|
||||||
date: 2023-09-27
|
date: 2023-09-27
|
||||||
tags:
|
tags:
|
||||||
- Merch
|
- Testing
|
||||||
- Rewrite
|
- Rewrite
|
||||||
- Bounties
|
- Bounties
|
||||||
---
|
---
|
||||||
@ -24,13 +24,13 @@ tags:
|
|||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
> TLDR; We have a fancy limited edition shirt. Contribute to [Malfunction Mania](https://documenso.com/blog/malfunction-mania) to get one.
|
> TLDR; We have a fancy limited edition shirt. Contribute to Malfunction Mania to get one.
|
||||||
|
|
||||||
We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) yesterday, and the first [issues](https://github.com/documenso/documenso/issues) are coming in. As mentioned, there will be dollar bounties, but we also wanted to celebrate entering the final stage of version 1.0 with something special. This is why we created this limited edition shirt. It will only be available during the runtime of Malfunction Mania. We have yet to set an exact end date, the next event in October, however, is looming, ready to end MM.
|
We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) yesterday, and the first [Issues](https://github.com/documenso/documenso/issues) are coming in. As mentioned, there will be dollar bounties, but we also wanted to celebrate entering the final stage of version 1.0 with something special. This is why we created the limited edition shirt for Malfunction Mania. It will only be available during the runtime of Malfunction Mania. We have yet to set an exact end date, the next event in October, however, is looming, ready to end MM.
|
||||||
|
|
||||||
## Documenso Merch Shop
|
## Documenso Merch Shop
|
||||||
|
|
||||||
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso.
|
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors.
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
<MdxNextImage
|
<MdxNextImage
|
||||||
@ -45,7 +45,7 @@ The shirt will be available in our [merch shop](https://documen.so/shop) via a u
|
|||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
## How earn the shirt
|
## How to get the shirt
|
||||||
|
|
||||||
If you have been following us, you know we are not big on formalities but highly value rewarding merit. That being said, any worthwhile contribution has a chance to get one. To inspire, here are a few ideas on how to contribute to securing one:
|
If you have been following us, you know we are not big on formalities but highly value rewarding merit. That being said, any worthwhile contribution has a chance to get one. To inspire, here are a few ideas on how to contribute to securing one:
|
||||||
|
|
||||||
@ -56,9 +56,7 @@ If you have been following us, you know we are not big on formalities but highly
|
|||||||
- Engage in discussion about the current version and its choices
|
- Engage in discussion about the current version and its choices
|
||||||
- Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging)
|
- Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging)
|
||||||
- Review the version with a video, stream, or screenshots and post about it
|
- Review the version with a video, stream, or screenshots and post about it
|
||||||
- Review existing or create missing documentation
|
- Review existing or create missing documenso
|
||||||
|
- ...
|
||||||
|
|
||||||
Best from Hamburg
|
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.**
|
||||||
Timur
|
|
||||||
|
|
||||||
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest updates about Malfunction Mania.**
|
|
||||||
|
|||||||
@ -56,14 +56,6 @@ const config = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
async rewrites() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/ingest/:path*',
|
|
||||||
destination: 'https://eu.posthog.com/:path*',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = withContentlayer(config);
|
module.exports = withContentlayer(config);
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1007 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 956 KiB |
@ -7,14 +7,14 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
|
|||||||
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
|
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
|
||||||
data: Record<string, string | number>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
|
export type FundingRaisedProps = HTMLAttributes<HTMLDivElement>;
|
||||||
const formattedData = data.map((item) => ({
|
|
||||||
|
export const FundingRaised = ({ className, ...props }: FundingRaisedProps) => {
|
||||||
|
const formattedData = FUNDING_RAISED.map((item) => ({
|
||||||
amount: Number(item.amount),
|
amount: Number(item.amount),
|
||||||
date: formatMonth(item.date as string),
|
date: formatMonth(item.date),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -7,25 +7,26 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
|
|||||||
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
export type BarMetricProps<T extends Record<string, unknown>> = HTMLAttributes<HTMLDivElement> & {
|
import { StargazersType } from './page';
|
||||||
data: T;
|
|
||||||
metricKey: keyof T[string];
|
export type MetricsDataKey = 'stars' | 'forks' | 'mergedPRs' | 'openIssues';
|
||||||
|
export type GithubMetricProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
data: StargazersType;
|
||||||
|
metricKey: MetricsDataKey;
|
||||||
title: string;
|
title: string;
|
||||||
label: string;
|
label: string;
|
||||||
chartHeight?: number;
|
chartHeight?: number;
|
||||||
extraInfo?: JSX.Element;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BarMetric = <T extends Record<string, Record<keyof T[string], unknown>>>({
|
export const GithubMetric = ({
|
||||||
className,
|
className,
|
||||||
data,
|
data,
|
||||||
metricKey,
|
metricKey,
|
||||||
title,
|
title,
|
||||||
label,
|
label,
|
||||||
chartHeight = 400,
|
chartHeight = 400,
|
||||||
extraInfo,
|
|
||||||
...props
|
...props
|
||||||
}: BarMetricProps<T>) => {
|
}: GithubMetricProps) => {
|
||||||
const formattedData = Object.keys(data)
|
const formattedData = Object.keys(data)
|
||||||
.map((key) => ({
|
.map((key) => ({
|
||||||
month: formatMonth(key),
|
month: formatMonth(key),
|
||||||
@ -35,10 +36,7 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-col', className)} {...props}>
|
<div className={cn('flex flex-col', className)} {...props}>
|
||||||
<div className="flex items-center px-4">
|
<h3 className="px-4 text-lg font-semibold">{title}</h3>
|
||||||
<h3 className="text-lg font-semibold">{title}</h3>
|
|
||||||
<span>{extraInfo}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow">
|
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow">
|
||||||
<ResponsiveContainer width="100%" height={chartHeight}>
|
<ResponsiveContainer width="100%" height={chartHeight}>
|
||||||
@ -52,7 +50,7 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
|
|||||||
formatter={(value) => [Number(value), label]}
|
formatter={(value) => [Number(value), label]}
|
||||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey={metricKey as string} fill="hsl(var(--primary))" label={label} />{' '}
|
<Bar dataKey={metricKey} fill="hsl(var(--primary))" label={label} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@ -1,14 +1,12 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
|
|
||||||
import { MetricCard } from '~/app/(marketing)/open/metric-card';
|
import { MetricCard } from '~/app/(marketing)/open/metric-card';
|
||||||
import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
|
import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
|
||||||
|
|
||||||
import { BarMetric } from './bar-metrics';
|
|
||||||
import { CapTable } from './cap-table';
|
import { CapTable } from './cap-table';
|
||||||
import { FundingRaised } from './funding-raised';
|
import { FundingRaised } from './funding-raised';
|
||||||
|
import { GithubMetric } from './gh-metrics';
|
||||||
import { TeamMembers } from './team-members';
|
import { TeamMembers } from './team-members';
|
||||||
import { OpenPageTooltip } from './tooltip';
|
|
||||||
|
|
||||||
export const revalidate = 3600;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
@ -31,16 +29,9 @@ const ZStargazersLiveResponse = z.record(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ZEarlyAdoptersResponse = z.record(
|
|
||||||
z.object({
|
|
||||||
id: z.number(),
|
|
||||||
time: z.string().datetime(),
|
|
||||||
earlyAdopters: z.number(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export type StargazersType = z.infer<typeof ZStargazersLiveResponse>;
|
export type StargazersType = z.infer<typeof ZStargazersLiveResponse>;
|
||||||
export type EarlyAdoptersType = z.infer<typeof ZEarlyAdoptersResponse>;
|
|
||||||
|
// const ZOpenPullRequestsResponse = ZMergedPullRequestsResponse;
|
||||||
|
|
||||||
export default async function OpenPage() {
|
export default async function OpenPage() {
|
||||||
const {
|
const {
|
||||||
@ -74,14 +65,6 @@ export default async function OpenPage() {
|
|||||||
.then(async (res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.then((res) => ZStargazersLiveResponse.parse(res));
|
.then((res) => ZStargazersLiveResponse.parse(res));
|
||||||
|
|
||||||
const EARLY_ADOPTERS_DATA = await fetch('https://stargrazer-live.onrender.com/api/stats/stripe', {
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (res) => res.json())
|
|
||||||
.then((res) => ZEarlyAdoptersResponse.parse(res));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
@ -124,20 +107,10 @@ export default async function OpenPage() {
|
|||||||
|
|
||||||
<SalaryBands className="col-span-12 lg:col-span-6" />
|
<SalaryBands className="col-span-12 lg:col-span-6" />
|
||||||
|
|
||||||
<FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
|
<FundingRaised className="col-span-12 lg:col-span-6" />
|
||||||
|
|
||||||
<CapTable className="col-span-12 lg:col-span-6" />
|
<CapTable className="col-span-12 lg:col-span-6" />
|
||||||
|
<GithubMetric
|
||||||
<BarMetric<EarlyAdoptersType>
|
|
||||||
data={EARLY_ADOPTERS_DATA}
|
|
||||||
metricKey="earlyAdopters"
|
|
||||||
title="Early Adopters"
|
|
||||||
label="Early Adopters"
|
|
||||||
className="col-span-12 lg:col-span-6"
|
|
||||||
extraInfo={<OpenPageTooltip />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<BarMetric<StargazersType>
|
|
||||||
data={STARGAZERS_DATA}
|
data={STARGAZERS_DATA}
|
||||||
metricKey="stars"
|
metricKey="stars"
|
||||||
title="Github: Total Stars"
|
title="Github: Total Stars"
|
||||||
@ -145,31 +118,29 @@ export default async function OpenPage() {
|
|||||||
className="col-span-12 lg:col-span-6"
|
className="col-span-12 lg:col-span-6"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BarMetric<StargazersType>
|
<GithubMetric
|
||||||
data={STARGAZERS_DATA}
|
data={STARGAZERS_DATA}
|
||||||
metricKey="mergedPRs"
|
metricKey="mergedPRs"
|
||||||
title="Github: Total Merged PRs"
|
title="Github: Total Merged PRs"
|
||||||
label="Merged PRs"
|
label="Merged PRs"
|
||||||
chartHeight={300}
|
chartHeight={300}
|
||||||
className="col-span-12 lg:col-span-6"
|
className="col-span-12 lg:col-span-4"
|
||||||
/>
|
/>
|
||||||
|
<GithubMetric
|
||||||
<BarMetric<StargazersType>
|
|
||||||
data={STARGAZERS_DATA}
|
data={STARGAZERS_DATA}
|
||||||
metricKey="forks"
|
metricKey="forks"
|
||||||
title="Github: Total Forks"
|
title="Github: Total Forks"
|
||||||
label="Forks"
|
label="Forks"
|
||||||
chartHeight={300}
|
chartHeight={300}
|
||||||
className="col-span-12 lg:col-span-6"
|
className="col-span-12 lg:col-span-4"
|
||||||
/>
|
/>
|
||||||
|
<GithubMetric
|
||||||
<BarMetric<StargazersType>
|
|
||||||
data={STARGAZERS_DATA}
|
data={STARGAZERS_DATA}
|
||||||
metricKey="openIssues"
|
metricKey="openIssues"
|
||||||
title="Github: Total Open Issues"
|
title="Github: Total Open Issues"
|
||||||
label="Open Issues"
|
label="Open Issues"
|
||||||
chartHeight={300}
|
chartHeight={300}
|
||||||
className="col-span-12 lg:col-span-6"
|
className="col-span-12 lg:col-span-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
|
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@documenso/ui/primitives/tooltip';
|
|
||||||
|
|
||||||
export function OpenPageTooltip() {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<svg
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
className="ml-2 mt-2.5"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM8.24992 4.49999C8.24992 4.9142 7.91413 5.24999 7.49992 5.24999C7.08571 5.24999 6.74992 4.9142 6.74992 4.49999C6.74992 4.08577 7.08571 3.74999 7.49992 3.74999C7.91413 3.74999 8.24992 4.08577 8.24992 4.49999ZM6.00003 5.99999H6.50003H7.50003C7.77618 5.99999 8.00003 6.22384 8.00003 6.49999V9.99999H8.50003H9.00003V11H8.50003H7.50003H6.50003H6.00003V9.99999H6.50003H7.00003V6.99999H6.50003H6.00003V5.99999Z"
|
|
||||||
fill="currentColor"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
August and earlier: Active subscribers. September and beyond: Numbers of active
|
|
||||||
subscriptions.
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -177,9 +177,7 @@ export const createSinglePlayerDocument = async (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Todo: Handle `downloadLink`
|
|
||||||
const template = createElement(DocumentSelfSignedEmailTemplate, {
|
const template = createElement(DocumentSelfSignedEmailTemplate, {
|
||||||
downloadLink: `${process.env.NEXT_PUBLIC_MARKETING_URL}/single-player-mode/${documentToken}`,
|
|
||||||
documentName: documentName,
|
documentName: documentName,
|
||||||
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
|
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
|
||||||
});
|
});
|
||||||
@ -197,6 +195,7 @@ export const createSinglePlayerDocument = async (
|
|||||||
subject: 'Document signed',
|
subject: 'Document signed',
|
||||||
html: render(template),
|
html: render(template),
|
||||||
text: render(template, { plainText: true }),
|
text: render(template, { plainText: true }),
|
||||||
|
attachments: [{ content: Buffer.from(pdfBytes), filename: documentName }],
|
||||||
});
|
});
|
||||||
|
|
||||||
return documentToken;
|
return documentToken;
|
||||||
|
|||||||
@ -29,14 +29,6 @@ const config = {
|
|||||||
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async rewrites() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/ingest/:path*',
|
|
||||||
destination: 'https://eu.posthog.com/:path*',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
BIN
apps/web/public/static/user-plus.png
Normal file
|
After Width: | Height: | Size: 367 B |
@ -12,7 +12,7 @@ export type AdminSectionLayoutProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
|
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
if (!isAdmin(user)) {
|
if (!isAdmin(user)) {
|
||||||
redirect('/documents');
|
redirect('/documents');
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default function Loading() {
|
|||||||
Documents
|
Documents
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||||
Loading Document...
|
Loading Document...
|
||||||
</h1>
|
</h1>
|
||||||
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
|
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
|
||||||
|
|||||||
@ -30,11 +30,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
redirect('/documents');
|
redirect('/documents');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const session = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const document = await getDocumentById({
|
const document = await getDocumentById({
|
||||||
id: documentId,
|
id: documentId,
|
||||||
userId: user.id,
|
userId: session.id,
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
if (!document || !document.documentData) {
|
if (!document || !document.documentData) {
|
||||||
@ -50,11 +50,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
const [recipients, fields] = await Promise.all([
|
const [recipients, fields] = await Promise.all([
|
||||||
await getRecipientsForDocument({
|
await getRecipientsForDocument({
|
||||||
documentId,
|
documentId,
|
||||||
userId: user.id,
|
userId: session.id,
|
||||||
}),
|
}),
|
||||||
await getFieldsForDocument({
|
await getFieldsForDocument({
|
||||||
documentId,
|
documentId,
|
||||||
userId: user.id,
|
userId: session.id,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -65,7 +65,10 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
Documents
|
Documents
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
<h1
|
||||||
|
className="mt-4 max-w-xs truncate text-2xl font-semibold md:text-3xl"
|
||||||
|
title={document.title}
|
||||||
|
>
|
||||||
{document.title}
|
{document.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@ -87,7 +90,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
<EditDocumentForm
|
<EditDocumentForm
|
||||||
className="mt-8"
|
className="mt-8"
|
||||||
document={document}
|
document={document}
|
||||||
user={user}
|
user={session}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
dataUrl={documentDataUrl}
|
dataUrl={documentDataUrl}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default function DocumentSentPage() {
|
|||||||
Documents
|
Documents
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||||
Loading Document...
|
Loading Document...
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export type DocumentsPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const stats = await getStats({
|
const stats = await getStats({
|
||||||
user,
|
user,
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export default async function AuthenticatedDashboardLayout({
|
|||||||
redirect('/signin');
|
redirect('/signin');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextAuthProvider session={session}>
|
<NextAuthProvider session={session}>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||||
|
|
||||||
export default async function BillingSettingsPage() {
|
export default async function BillingSettingsPage() {
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const isBillingEnabled = await getServerComponentFlag('app_billing');
|
const isBillingEnabled = await getServerComponentFlag('app_billing');
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
|||||||
import { PasswordForm } from '~/components/forms/password';
|
import { PasswordForm } from '~/components/forms/password';
|
||||||
|
|
||||||
export default async function PasswordSettingsPage() {
|
export default async function PasswordSettingsPage() {
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
|||||||
import { ProfileForm } from '~/components/forms/profile';
|
import { ProfileForm } from '~/components/forms/profile';
|
||||||
|
|
||||||
export default async function ProfileSettingsPage() {
|
export default async function ProfileSettingsPage() {
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export default async function CompletedSigningPage({
|
|||||||
recipient.email;
|
recipient.email;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44">
|
<div className="flex flex-col items-center pt-24 lg:pt-36 xl:pt-44">
|
||||||
{/* Card with recipient */}
|
{/* Card with recipient */}
|
||||||
<SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />
|
<SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />
|
||||||
|
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent position="end">
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Share</DialogTitle>
|
<DialogTitle>Share</DialogTitle>
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
|
|||||||
<span className="font-medium text-blue-400">@documenso</span>
|
<span className="font-medium text-blue-400">@documenso</span>
|
||||||
. Check it out!
|
. Check it out!
|
||||||
<span className="mt-2 block" />
|
<span className="mt-2 block" />
|
||||||
<span className="break-all font-medium text-blue-400">
|
<span className="font-medium text-blue-400">
|
||||||
{window.location.origin}/share/{shareLink?.slug || '...'}
|
{window.location.origin}/share/{shareLink?.slug || '...'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
||||||
@ -28,7 +27,6 @@ export type SigningFormProps = {
|
|||||||
|
|
||||||
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
|
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: session } = useSession();
|
|
||||||
|
|
||||||
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
||||||
|
|
||||||
@ -62,11 +60,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className={cn(
|
className={cn(
|
||||||
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
|
'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[80rem] flex-col rounded-xl border px-4 py-6',
|
||||||
{
|
|
||||||
'top-20 max-h-[min(68rem,calc(100vh-6rem))]': session,
|
|
||||||
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !session,
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
>
|
>
|
||||||
@ -126,7 +120,6 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@ -10,11 +10,11 @@ export type SigningLayoutProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function SigningLayout({ children }: SigningLayoutProps) {
|
export default async function SigningLayout({ children }: SigningLayoutProps) {
|
||||||
const { user, session } = await getServerComponentSession();
|
const user = await getServerComponentSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextAuthProvider session={session}>
|
<NextAuthProvider>
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen overflow-hidden">
|
||||||
{user && <AuthenticatedHeader user={user} />}
|
{user && <AuthenticatedHeader user={user} />}
|
||||||
|
|
||||||
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">{children}</main>
|
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">{children}</main>
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
.then((buffer) => Buffer.from(buffer).toString('base64'))
|
.then((buffer) => Buffer.from(buffer).toString('base64'))
|
||||||
.then((data) => `data:application/pdf;base64,${data}`);
|
.then((data) => `data:application/pdf;base64,${data}`);
|
||||||
|
|
||||||
const { user } = await getServerComponentSession();
|
const user = await getServerComponentSession();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
document.status === DocumentStatus.COMPLETED ||
|
document.status === DocumentStatus.COMPLETED ||
|
||||||
@ -62,7 +62,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
|
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
|
||||||
<div className="mx-auto w-full max-w-screen-xl">
|
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||||
{document.title}
|
{document.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
import NotFoundPartial from '~/components/partials/not-found';
|
import NotFoundPartial from '~/components/partials/not-found';
|
||||||
|
|
||||||
export default async function NotFound() {
|
export default async function NotFound() {
|
||||||
const { session } = await getServerComponentSession();
|
const session = await getServerComponentSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotFoundPartial>
|
<NotFoundPartial>
|
||||||
|
|||||||
@ -11,10 +11,10 @@ export type AddFieldsActionInput = TAddFieldsFormSchema & {
|
|||||||
export const addFields = async ({ documentId, fields }: AddFieldsActionInput) => {
|
export const addFields = async ({ documentId, fields }: AddFieldsActionInput) => {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { id: userId } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
await setFieldsForDocument({
|
await setFieldsForDocument({
|
||||||
userId: user.id,
|
userId,
|
||||||
documentId,
|
documentId,
|
||||||
fields: fields.map((field) => ({
|
fields: fields.map((field) => ({
|
||||||
id: field.nativeId,
|
id: field.nativeId,
|
||||||
|
|||||||
@ -11,10 +11,10 @@ export type AddSignersActionInput = TAddSignersFormSchema & {
|
|||||||
export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => {
|
export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { id: userId } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
await setRecipientsForDocument({
|
await setRecipientsForDocument({
|
||||||
userId: user.id,
|
userId,
|
||||||
documentId,
|
documentId,
|
||||||
recipients: signers.map((signer) => ({
|
recipients: signers.map((signer) => ({
|
||||||
id: signer.nativeId,
|
id: signer.nativeId,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
|||||||
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
|
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { id: userId } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
if (email.message || email.subject) {
|
if (email.message || email.subject) {
|
||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
@ -23,7 +23,7 @@ export const completeDocument = async ({ documentId, email }: CompleteDocumentAc
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await sendDocument({
|
return await sendDocument({
|
||||||
userId: user.id,
|
userId,
|
||||||
documentId,
|
documentId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
26
package-lock.json
generated
@ -16958,29 +16958,6 @@
|
|||||||
"node": ">=0.10.5"
|
"node": ">=0.10.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/resend": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/resend/-/resend-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-it8TIDVT+/gAiJsUlv2tdHuvzwCCv4Zwu+udDqIm/dIuByQwe68TtFDcPccxqpSVVrNCBxxXLzsdT1tsV+P3GA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@react-email/render": "0.0.7",
|
|
||||||
"type-fest": "3.13.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/resend/node_modules/type-fest": {
|
|
||||||
"version": "3.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.0.tgz",
|
|
||||||
"integrity": "sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.16"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.2",
|
"version": "1.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
||||||
@ -19796,8 +19773,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-email/components": "^0.0.7",
|
"@react-email/components": "^0.0.7",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"react-email": "^1.9.4",
|
"react-email": "^1.9.4"
|
||||||
"resend": "^1.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { createTransport } from 'nodemailer';
|
import { createTransport } from 'nodemailer';
|
||||||
|
|
||||||
import { MailChannelsTransport } from './transports/mailchannels';
|
import { MailChannelsTransport } from './transports/mailchannels';
|
||||||
import { ResendTransport } from './transports/resend';
|
|
||||||
|
|
||||||
const getTransport = () => {
|
const getTransport = () => {
|
||||||
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth';
|
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth';
|
||||||
@ -15,14 +14,6 @@ const getTransport = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transport === 'resend') {
|
|
||||||
return createTransport(
|
|
||||||
ResendTransport.makeTransport({
|
|
||||||
apiKey: process.env.NEXT_PRIVATE_RESEND_API_KEY || '',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transport === 'smtp-api') {
|
if (transport === 'smtp-api') {
|
||||||
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
|
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -19,8 +19,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-email/components": "^0.0.7",
|
"@react-email/components": "^0.0.7",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"react-email": "^1.9.4",
|
"react-email": "^1.9.4"
|
||||||
"resend": "^1.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
|
|||||||
BIN
packages/email/static/user-plus.png
Normal file
|
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 * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export interface TemplateDocumentCompletedProps {
|
export interface TemplateDocumentCompletedProps {
|
||||||
downloadLink: string;
|
downloadLink: string;
|
||||||
documentName: string;
|
documentName: string;
|
||||||
@ -27,27 +29,20 @@ export const TemplateDocumentCompleted = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section>
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<Row className="table-fixed">
|
|
||||||
<Column />
|
|
||||||
|
|
||||||
<Column>
|
<Section>
|
||||||
|
<Section className="mb-4">
|
||||||
|
<Column align="center">
|
||||||
|
<Text className="text-base font-semibold text-[#7AC455]">
|
||||||
<Img
|
<Img
|
||||||
className="h-42 mx-auto"
|
src={getAssetUrl('/static/completed.png')}
|
||||||
src={getAssetUrl('/static/document.png')}
|
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||||
alt="Documenso"
|
|
||||||
/>
|
/>
|
||||||
</Column>
|
|
||||||
|
|
||||||
<Column />
|
|
||||||
</Row>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section>
|
|
||||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
|
|
||||||
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
|
||||||
Completed
|
Completed
|
||||||
</Text>
|
</Text>
|
||||||
|
</Column>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||||
“{documentName}” was signed by all signers
|
“{documentName}” was signed by all signers
|
||||||
@ -66,10 +61,13 @@ export const TemplateDocumentCompleted = ({
|
|||||||
Review
|
Review
|
||||||
</Button> */}
|
</Button> */}
|
||||||
<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}
|
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
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</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 * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export interface TemplateDocumentInviteProps {
|
export interface TemplateDocumentInviteProps {
|
||||||
inviterName: string;
|
inviterName: string;
|
||||||
inviterEmail: string;
|
inviterEmail: string;
|
||||||
@ -16,10 +18,6 @@ export const TemplateDocumentInvite = ({
|
|||||||
signDocumentLink,
|
signDocumentLink,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
}: TemplateDocumentInviteProps) => {
|
}: TemplateDocumentInviteProps) => {
|
||||||
const getAssetUrl = (path: string) => {
|
|
||||||
return new URL(path, assetBaseUrl).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tailwind
|
<Tailwind
|
||||||
config={{
|
config={{
|
||||||
@ -30,21 +28,7 @@ export const TemplateDocumentInvite = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section className="mt-4">
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<Row className="table-fixed">
|
|
||||||
<Column />
|
|
||||||
|
|
||||||
<Column>
|
|
||||||
<Img
|
|
||||||
className="h-42 mx-auto"
|
|
||||||
src={getAssetUrl('/static/document.png')}
|
|
||||||
alt="Documenso"
|
|
||||||
/>
|
|
||||||
</Column>
|
|
||||||
|
|
||||||
<Column />
|
|
||||||
</Row>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section>
|
<Section>
|
||||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
<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 * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export interface TemplateDocumentPendingProps {
|
export interface TemplateDocumentPendingProps {
|
||||||
documentName: string;
|
documentName: string;
|
||||||
assetBaseUrl: string;
|
assetBaseUrl: string;
|
||||||
@ -25,27 +27,20 @@ export const TemplateDocumentPending = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section>
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<Row className="table-fixed">
|
|
||||||
<Column />
|
|
||||||
|
|
||||||
<Column>
|
<Section>
|
||||||
|
<Section className="mb-4">
|
||||||
|
<Column align="center">
|
||||||
|
<Text className="text-base font-semibold text-blue-500">
|
||||||
<Img
|
<Img
|
||||||
className="h-42 mx-auto"
|
src={getAssetUrl('/static/clock.png')}
|
||||||
src={getAssetUrl('/static/document.png')}
|
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||||
alt="Documenso"
|
|
||||||
/>
|
/>
|
||||||
</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
|
Waiting for others
|
||||||
</Text>
|
</Text>
|
||||||
|
</Column>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||||
“{documentName}” has been signed
|
“{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 * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export interface TemplateDocumentSelfSignedProps {
|
export interface TemplateDocumentSelfSignedProps {
|
||||||
downloadLink: string;
|
|
||||||
documentName: string;
|
documentName: string;
|
||||||
assetBaseUrl: string;
|
assetBaseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateDocumentSelfSigned = ({
|
export const TemplateDocumentSelfSigned = ({
|
||||||
downloadLink,
|
|
||||||
documentName,
|
documentName,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
}: TemplateDocumentSelfSignedProps) => {
|
}: TemplateDocumentSelfSignedProps) => {
|
||||||
|
const signUpUrl = `${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`;
|
||||||
|
|
||||||
const getAssetUrl = (path: string) => {
|
const getAssetUrl = (path: string) => {
|
||||||
return new URL(path, assetBaseUrl).toString();
|
return new URL(path, assetBaseUrl).toString();
|
||||||
};
|
};
|
||||||
@ -27,39 +29,59 @@ export const TemplateDocumentSelfSigned = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section className="flex-row items-center justify-center">
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<div className="flex items-center justify-center p-4">
|
|
||||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
|
<Section className="flex-row items-center justify-center">
|
||||||
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
<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
|
Completed
|
||||||
</Text>
|
</Text>
|
||||||
|
</Column>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<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}”
|
You have signed “{documentName}”
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
<Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
|
||||||
Check out our plans to access the full suite of features.
|
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>
|
</Text>
|
||||||
|
|
||||||
<Section className="mb-6 mt-8 text-center">
|
<Section className="mb-6 mt-8 text-center">
|
||||||
<Button
|
<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"
|
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
|
View plans
|
||||||
</Button>
|
</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>
|
||||||
</Section>
|
</Section>
|
||||||
</Tailwind>
|
</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 * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export type TemplateForgotPasswordProps = {
|
export type TemplateForgotPasswordProps = {
|
||||||
resetPasswordLink: string;
|
resetPasswordLink: string;
|
||||||
assetBaseUrl: string;
|
assetBaseUrl: string;
|
||||||
@ -11,10 +13,6 @@ export const TemplateForgotPassword = ({
|
|||||||
resetPasswordLink,
|
resetPasswordLink,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
}: TemplateForgotPasswordProps) => {
|
}: TemplateForgotPasswordProps) => {
|
||||||
const getAssetUrl = (path: string) => {
|
|
||||||
return new URL(path, assetBaseUrl).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tailwind
|
<Tailwind
|
||||||
config={{
|
config={{
|
||||||
@ -25,11 +23,9 @@ export const TemplateForgotPassword = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<div className="flex items-center justify-center p-4">
|
|
||||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<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">
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
Forgot your password?
|
Forgot your password?
|
||||||
</Text>
|
</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 * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export interface TemplateResetPasswordProps {
|
export interface TemplateResetPasswordProps {
|
||||||
userName: string;
|
userName: string;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
@ -9,10 +11,6 @@ export interface TemplateResetPasswordProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
|
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
|
||||||
const getAssetUrl = (path: string) => {
|
|
||||||
return new URL(path, assetBaseUrl).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tailwind
|
<Tailwind
|
||||||
config={{
|
config={{
|
||||||
@ -23,11 +21,9 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<div className="flex items-center justify-center p-4">
|
|
||||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<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">
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
Password updated!
|
Password updated!
|
||||||
</Text>
|
</Text>
|
||||||
@ -35,6 +31,15 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
|
|||||||
<Text className="my-1 text-center text-base text-slate-400">
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
Your password has been updated.
|
Your password has been updated.
|
||||||
</Text>
|
</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>
|
</Section>
|
||||||
</Tailwind>
|
</Tailwind>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
TemplateDocumentCompleted,
|
TemplateDocumentCompleted,
|
||||||
TemplateDocumentCompletedProps,
|
TemplateDocumentCompletedProps,
|
||||||
} from '../template-components/template-document-completed';
|
} 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>;
|
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import {
|
|||||||
TemplateDocumentInvite,
|
TemplateDocumentInvite,
|
||||||
TemplateDocumentInviteProps,
|
TemplateDocumentInviteProps,
|
||||||
} from '../template-components/template-document-invite';
|
} 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> & {
|
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
||||||
customBody?: string;
|
customBody?: string;
|
||||||
|
|||||||
@ -15,12 +15,11 @@ import {
|
|||||||
TemplateDocumentSelfSigned,
|
TemplateDocumentSelfSigned,
|
||||||
TemplateDocumentSelfSignedProps,
|
TemplateDocumentSelfSignedProps,
|
||||||
} from '../template-components/template-document-self-signed';
|
} 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 type DocumentSelfSignedTemplateProps = TemplateDocumentSelfSignedProps;
|
||||||
|
|
||||||
export const DocumentSelfSignedEmailTemplate = ({
|
export const DocumentSelfSignedEmailTemplate = ({
|
||||||
downloadLink = 'https://documenso.com',
|
|
||||||
documentName = 'Open Source Pledge.pdf',
|
documentName = 'Open Source Pledge.pdf',
|
||||||
assetBaseUrl = 'http://localhost:3002',
|
assetBaseUrl = 'http://localhost:3002',
|
||||||
}: DocumentSelfSignedTemplateProps) => {
|
}: DocumentSelfSignedTemplateProps) => {
|
||||||
@ -54,7 +53,6 @@ export const DocumentSelfSignedEmailTemplate = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TemplateDocumentSelfSigned
|
<TemplateDocumentSelfSigned
|
||||||
downloadLink={downloadLink}
|
|
||||||
documentName={documentName}
|
documentName={documentName}
|
||||||
assetBaseUrl={assetBaseUrl}
|
assetBaseUrl={assetBaseUrl}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
import TemplateFooter from '../template-components/template-footer';
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
import {
|
import {
|
||||||
TemplateForgotPassword,
|
TemplateForgotPassword,
|
||||||
TemplateForgotPasswordProps,
|
TemplateForgotPasswordProps,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
import TemplateFooter from '../template-components/template-footer';
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
import {
|
import {
|
||||||
TemplateResetPassword,
|
TemplateResetPassword,
|
||||||
TemplateResetPasswordProps,
|
TemplateResetPasswordProps,
|
||||||
|
|||||||
@ -1,145 +0,0 @@
|
|||||||
import { type SentMessageInfo, type Transport } from 'nodemailer';
|
|
||||||
import type Mail from 'nodemailer/lib/mailer';
|
|
||||||
import type MailMessage from 'nodemailer/lib/mailer/mail-message';
|
|
||||||
import { Resend } from 'resend';
|
|
||||||
|
|
||||||
const VERSION = '1.0.0';
|
|
||||||
|
|
||||||
type ResendTransportOptions = {
|
|
||||||
apiKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ResendResponseError = {
|
|
||||||
statusCode: number;
|
|
||||||
name: string;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isResendResponseError = (error: unknown): error is ResendResponseError => {
|
|
||||||
// We could use Zod here, but it's not worth the extra bundle size
|
|
||||||
return (
|
|
||||||
typeof error === 'object' &&
|
|
||||||
error !== null &&
|
|
||||||
'statusCode' in error &&
|
|
||||||
typeof error.statusCode === 'number' &&
|
|
||||||
'name' in error &&
|
|
||||||
typeof error.name === 'string' &&
|
|
||||||
'message' in error &&
|
|
||||||
typeof error.message === 'string'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transport for sending email via the Resend SDK.
|
|
||||||
*/
|
|
||||||
export class ResendTransport implements Transport<SentMessageInfo> {
|
|
||||||
public name = 'ResendMailTransport';
|
|
||||||
public version = VERSION;
|
|
||||||
|
|
||||||
private _client: Resend;
|
|
||||||
private _options: ResendTransportOptions;
|
|
||||||
|
|
||||||
public static makeTransport(options: Partial<ResendTransportOptions>) {
|
|
||||||
return new ResendTransport(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(options: Partial<ResendTransportOptions>) {
|
|
||||||
const { apiKey = '' } = options;
|
|
||||||
|
|
||||||
this._options = {
|
|
||||||
apiKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._client = new Resend(apiKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public send(mail: MailMessage, callback: (_err: Error | null, _info: SentMessageInfo) => void) {
|
|
||||||
if (!mail.data.to || !mail.data.from) {
|
|
||||||
return callback(new Error('Missing required fields "to" or "from"'), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._client
|
|
||||||
.sendEmail({
|
|
||||||
subject: mail.data.subject ?? '',
|
|
||||||
from: this.toResendFromAddress(mail.data.from),
|
|
||||||
to: this.toResendAddresses(mail.data.to),
|
|
||||||
cc: this.toResendAddresses(mail.data.cc),
|
|
||||||
bcc: this.toResendAddresses(mail.data.bcc),
|
|
||||||
html: mail.data.html?.toString() || '',
|
|
||||||
text: mail.data.text?.toString() || '',
|
|
||||||
attachments: this.toResendAttachments(mail.data.attachments),
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (isResendResponseError(response)) {
|
|
||||||
throw new Error(`[${response.statusCode}]: ${response.name} ${response.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, response);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
callback(error, null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private toResendAddresses(addresses: Mail.Options['to']) {
|
|
||||||
if (!addresses) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof addresses === 'string') {
|
|
||||||
return [addresses];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(addresses)) {
|
|
||||||
return addresses.map((address) => {
|
|
||||||
if (typeof address === 'string') {
|
|
||||||
return address;
|
|
||||||
}
|
|
||||||
|
|
||||||
return address.address;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return [addresses.address];
|
|
||||||
}
|
|
||||||
|
|
||||||
private toResendFromAddress(address: Mail.Options['from']) {
|
|
||||||
if (!address) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof address === 'string') {
|
|
||||||
return address;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${address.name} <${address.address}>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private toResendAttachments(attachments: Mail.Options['attachments']) {
|
|
||||||
if (!attachments) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return attachments.map((attachment) => {
|
|
||||||
if (!attachment.filename || !attachment.content) {
|
|
||||||
throw new Error('Attachment is missing filename or content');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof attachment.content === 'string') {
|
|
||||||
return {
|
|
||||||
filename: attachment.filename,
|
|
||||||
content: Buffer.from(attachment.content),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attachment.content instanceof Buffer) {
|
|
||||||
return {
|
|
||||||
filename: attachment.filename,
|
|
||||||
content: attachment.content,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Attachment content must be a string or a buffer');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@documenso/tsconfig/react-library.json",
|
"extends": "@documenso/tsconfig/react-library.json",
|
||||||
"compilerOptions": {
|
|
||||||
"types": [
|
|
||||||
"@documenso/tsconfig/process-env.d.ts",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
|
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
|
||||||
"exclude": ["dist", "build", "node_modules"]
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { APP_BASE_URL } from './app';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The flag name for global session recording feature flag.
|
* The flag name for global session recording feature flag.
|
||||||
*/
|
*/
|
||||||
@ -25,7 +23,7 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
|
|||||||
*/
|
*/
|
||||||
export function extractPostHogConfig(): { key: string; host: string } | null {
|
export function extractPostHogConfig(): { key: string; host: string } | null {
|
||||||
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||||
const postHogHost = `${APP_BASE_URL}/ingest`;
|
const postHogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
|
||||||
|
|
||||||
if (!postHogKey || !postHogHost) {
|
if (!postHogKey || !postHogHost) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
|
|||||||
const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS);
|
const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS);
|
||||||
|
|
||||||
if (!session || !session.user?.email) {
|
if (!session || !session.user?.email) {
|
||||||
return { user: null, session: null };
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
@ -24,14 +24,14 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { user, session };
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerComponentSession = async () => {
|
export const getServerComponentSession = async () => {
|
||||||
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
|
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
|
||||||
|
|
||||||
if (!session || !session.user?.email) {
|
if (!session || !session.user?.email) {
|
||||||
return { user: null, session: null };
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
@ -40,15 +40,15 @@ export const getServerComponentSession = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { user, session };
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRequiredServerComponentSession = async () => {
|
export const getRequiredServerComponentSession = async () => {
|
||||||
const { user, session } = await getServerComponentSession();
|
const session = await getServerComponentSession();
|
||||||
|
|
||||||
if (!user || !session) {
|
if (!session) {
|
||||||
throw new Error('No session found');
|
throw new Error('No session found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user, session };
|
return session;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
|
console.warn(`No recipient found for token ${token}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -57,13 +57,12 @@ export const setRecipientsForDocument = async ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...recipient,
|
...recipient,
|
||||||
_persisted: existing,
|
...existing,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((recipient) => {
|
.filter((recipient) => {
|
||||||
return (
|
return (
|
||||||
recipient._persisted?.sendStatus !== SendStatus.SENT &&
|
recipient.sendStatus !== SendStatus.SENT && recipient.signingStatus !== SigningStatus.SIGNED
|
||||||
recipient._persisted?.signingStatus !== SigningStatus.SIGNED
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,7 +72,7 @@ export const setRecipientsForDocument = async ({
|
|||||||
linkedRecipients.map((recipient) =>
|
linkedRecipients.map((recipient) =>
|
||||||
prisma.recipient.upsert({
|
prisma.recipient.upsert({
|
||||||
where: {
|
where: {
|
||||||
id: recipient._persisted?.id ?? -1,
|
id: recipient.id ?? -1,
|
||||||
documentId,
|
documentId,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { alphaid } from '../id';
|
|||||||
export const getPresignPostUrl = async (fileName: string, contentType: string) => {
|
export const getPresignPostUrl = async (fileName: string, contentType: string) => {
|
||||||
const client = getS3Client();
|
const client = getS3Client();
|
||||||
|
|
||||||
const { user } = await getServerComponentSession();
|
const user = await getServerComponentSession();
|
||||||
|
|
||||||
// Get the basename and extension for the file
|
// Get the basename and extension for the file
|
||||||
const { name, ext } = path.parse(fileName);
|
const { name, ext } = path.parse(fileName);
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "PasswordResetToken" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"token" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"userId" INTEGER NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- Added the required column `expiry` to the `PasswordResetToken` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "PasswordResetToken" ADD COLUMN "expiry" TIMESTAMP(3) NOT NULL;
|
|
||||||
@ -1,5 +1,3 @@
|
|||||||
DROP TABLE IF EXISTS "PasswordResetToken" CASCADE;
|
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "PasswordResetToken" (
|
CREATE TABLE "PasswordResetToken" (
|
||||||
"id" SERIAL NOT NULL,
|
"id" SERIAL NOT NULL,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { CreateNextContextOptions } from '@trpc/server/adapters/next';
|
|||||||
import { getServerSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getServerSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
|
|
||||||
export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) => {
|
export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) => {
|
||||||
const { session, user } = await getServerSession({ req, res });
|
const session = await getServerSession({ req, res });
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return {
|
return {
|
||||||
@ -12,16 +12,9 @@ export const createTrpcContext = async ({ req, res }: CreateNextContextOptions)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return {
|
|
||||||
session: null,
|
|
||||||
user: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
user,
|
user: session,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
4
packages/tsconfig/process-env.d.ts
vendored
@ -27,9 +27,7 @@ declare namespace NodeJS {
|
|||||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string;
|
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string;
|
||||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string;
|
NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string;
|
||||||
|
|
||||||
NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'resend' | 'smtp-auth' | 'smtp-api';
|
NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'smtp-auth' | 'smtp-api';
|
||||||
|
|
||||||
NEXT_PRIVATE_RESEND_API_KEY?: string;
|
|
||||||
|
|
||||||
NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string;
|
NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string;
|
||||||
NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string;
|
NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string;
|
||||||
|
|||||||
@ -11,19 +11,9 @@ const Dialog = DialogPrimitive.Root;
|
|||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger;
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
||||||
const DialogPortal = ({
|
const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => (
|
||||||
className,
|
|
||||||
children,
|
|
||||||
position = 'start',
|
|
||||||
...props
|
|
||||||
}: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' }) => (
|
|
||||||
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
||||||
<div
|
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||||
className={cn('fixed inset-0 z-50 flex justify-center sm:items-center', {
|
|
||||||
'items-start': position === 'start',
|
|
||||||
'items-end': position === 'end',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</DialogPrimitive.Portal>
|
</DialogPrimitive.Portal>
|
||||||
@ -49,14 +39,14 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { position?: 'start' | 'end' }
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
>(({ className, children, position = 'start', ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<DialogPortal position={position}>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-background animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg',
|
'bg-background animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -50,7 +50,6 @@
|
|||||||
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS",
|
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS",
|
||||||
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING",
|
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING",
|
||||||
"NEXT_PRIVATE_SMTP_TRANSPORT",
|
"NEXT_PRIVATE_SMTP_TRANSPORT",
|
||||||
"NEXT_PRIVATE_RESEND_API_KEY",
|
|
||||||
"NEXT_PRIVATE_MAILCHANNELS_API_KEY",
|
"NEXT_PRIVATE_MAILCHANNELS_API_KEY",
|
||||||
"NEXT_PRIVATE_MAILCHANNELS_ENDPOINT",
|
"NEXT_PRIVATE_MAILCHANNELS_ENDPOINT",
|
||||||
"NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN",
|
"NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN",
|
||||||
|
|||||||