Compare commits

..

20 Commits

Author SHA1 Message Date
Mythie
de880aa821 v1.7.2-rc.4 2024-11-05 13:50:01 +11:00
Mythie
dc5723c386 chore: add i18n lang to document deleted email 2024-11-05 13:44:00 +11:00
Lucas Smith
c57d1dc55d chore: add translations (#1443)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-11-05 13:07:55 +11:00
Lucas Smith
4dd95016b1 feat: i18n for emails (#1442)
## Description

Support setting a document language that will control the language used
for sending emails to recipients. Additional work has been done to
convert all emails to using our i18n implementation so we can later add
controls for sending other kinds of emails in a users target language.

## Related Issue

N/A

## Changes Made

- Added `<Trans>` and `msg` macros to emails
- Introduced a new `renderEmailWithI18N` utility in the lib package
- Updated all emails to use the `<Tailwind>` component at the top level
due to rendering constraints
- Updated the `i18n.server.tsx` file to not use a top level await

## Testing Performed

- Configured document language and verified emails were sent in the
expected language
- Created a document from a template and verified that the templates
language was transferred to the document
2024-11-05 11:52:54 +11:00
David Nguyen
04b1ce1aab fix: missing not found page for deleted documents (#1424) 2024-11-04 22:09:52 +09:00
David Nguyen
885349ad94 fix: missing signing order when using templates (#1425) 2024-11-03 20:17:41 +09:00
David Nguyen
28514ba2e7 fix: duplicate templates (#1434) 2024-11-01 21:29:38 +11:00
Lucas Smith
8aa6d8e602 chore: add translations (#1433) 2024-11-01 13:22:51 +09:00
David Nguyen
378e515843 chore: extract translations 2024-11-01 12:56:07 +09:00
David Nguyen
f42e600e3f chore: update workflow 2024-11-01 12:37:54 +09:00
David Nguyen
88eaec91c9 chore: extract translations 2024-11-01 11:27:09 +09:00
David Nguyen
f199183c78 feat: improve translation coverage (#1427)
Improves translation coverage across the app.
2024-11-01 10:57:32 +11:00
Mythie
0cee07aed3 v1.7.2-rc.3 2024-10-31 15:33:03 +11:00
Mythie
f76f87ff1c fix: use key for expansion on embeds 2024-10-31 15:31:40 +11:00
Mythie
6020336792 v1.7.2-rc.2 2024-10-30 14:37:50 +11:00
Mythie
634b30aa54 fix: signature flickering during embed 2024-10-30 14:36:35 +11:00
David Nguyen
7fc497a642 fix: translation upload token (#1423) 2024-10-29 19:55:49 +09:00
Andre G
e30ceeb038 style: update common.po (#1402)
Update translations
2024-10-28 11:26:12 +09:00
Andre G
872762661a style: Update web.po (#1403)
Update translations
2024-10-28 11:23:08 +09:00
David Nguyen
5fcd8610c9 fix: translate extract command (#1394)
Change how the translate extract command is run on build
2024-10-28 11:21:49 +09:00
223 changed files with 5556 additions and 3342 deletions

View File

@@ -21,7 +21,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ github.event.pull_request.head.ref }} token: ${{ secrets.GH_PAT }}
- uses: ./.github/actions/node-install - uses: ./.github/actions/node-install

View File

@@ -1 +1,36 @@
# @documenso/documentation This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3002](http://localhost:3002) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -1,11 +1,11 @@
{ {
"name": "@documenso/marketing", "name": "@documenso/marketing",
"version": "1.7.2-rc.1", "version": "1.7.2-rc.4",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "next dev -p 3001", "dev": "next dev -p 3001",
"build": "turbo run translate:extract && turbo run translate:compile && next build", "build": "npm run translate:extract --prefix ../../ && turbo run translate:compile && next build",
"start": "next start -p 3001", "start": "next start -p 3001",
"lint": "next lint", "lint": "next lint",
"lint:fix": "next lint --fix", "lint:fix": "next lint --fix",

View File

@@ -30,8 +30,8 @@ const mdxComponents: MDXComponents = {
* *
* Will render the document if it exists, otherwise will return a 404. * Will render the document if it exists, otherwise will return a 404.
*/ */
export default function ContentPage({ params }: { params: { content: string } }) { export default async function ContentPage({ params }: { params: { content: string } }) {
setupI18nSSR(); await setupI18nSSR();
const post = allDocuments.find((post) => post._raw.flattenedPath === params.content); const post = allDocuments.find((post) => post._raw.flattenedPath === params.content);

View File

@@ -48,8 +48,8 @@ const mdxComponents: MDXComponents = {
), ),
}; };
export default function BlogPostPage({ params }: { params: { post: string } }) { export default async function BlogPostPage({ params }: { params: { post: string } }) {
setupI18nSSR(); await setupI18nSSR();
const post = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`); const post = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`);

View File

@@ -9,8 +9,8 @@ export const metadata: Metadata = {
title: 'Blog', title: 'Blog',
}; };
export default function BlogPage() { export default async function BlogPage() {
const { i18n } = setupI18nSSR(); const { i18n } = await setupI18nSSR();
const blogPosts = allBlogPosts.sort((a, b) => { const blogPosts = allBlogPosts.sort((a, b) => {
const dateA = new Date(a.date); const dateA = new Date(a.date);

View File

@@ -1,7 +1,6 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { z } from 'zod'; import { z } from 'zod';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
@@ -131,9 +130,9 @@ const fetchEarlyAdopters = async () => {
}; };
export default async function OpenPage() { export default async function OpenPage() {
setupI18nSSR(); const { i18n } = await setupI18nSSR();
const { _ } = useLingui(); const { _ } = i18n;
const [ const [
{ forks_count: forksCount, stargazers_count: stargazersCount }, { forks_count: forksCount, stargazers_count: stargazersCount },

View File

@@ -26,7 +26,7 @@ const fontCaveat = Caveat({
}); });
export default async function IndexPage() { export default async function IndexPage() {
setupI18nSSR(); await setupI18nSSR();
const starCount = await fetch('https://api.github.com/repos/documenso/documenso', { const starCount = await fetch('https://api.github.com/repos/documenso/documenso', {
headers: { headers: {

View File

@@ -30,8 +30,8 @@ export type PricingPageProps = {
}; };
}; };
export default function PricingPage() { export default async function PricingPage() {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<div className="mt-6 sm:mt-12"> <div className="mt-6 sm:mt-12">

View File

@@ -14,8 +14,8 @@ export const dynamic = 'force-dynamic';
// !: This entire file is a hack to get around failed prerendering of // !: This entire file is a hack to get around failed prerendering of
// !: the Single Player Mode page. This regression was introduced during // !: the Single Player Mode page. This regression was introduced during
// !: the upgrade of Next.js to v13.5.x. // !: the upgrade of Next.js to v13.5.x.
export default function SingleplayerPage() { export default async function SingleplayerPage() {
setupI18nSSR(); await setupI18nSSR();
return <SinglePlayerClient />; return <SinglePlayerClient />;
} }

View File

@@ -56,7 +56,7 @@ export function generateMetadata() {
export default async function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getAllAnonymousFlags(); const flags = await getAllAnonymousFlags();
const { lang, locales, i18n } = setupI18nSSR(); const { lang, locales, i18n } = await setupI18nSSR();
return ( return (
<html <html

View File

@@ -1,40 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for commiting if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -1 +0,0 @@
# @documenso/openpage-api

View File

@@ -1,36 +0,0 @@
import type { NextRequest } from 'next/server';
import cors from '@/lib/cors';
const paths = [
{ path: '/total-prs', description: 'Total GitHub Merged PRs' },
{ path: '/total-stars', description: 'Total GitHub Stars' },
{ path: '/total-forks', description: 'Total GitHub Forks' },
{ path: '/total-issues', description: 'Total GitHub Issues' },
];
export function GET(request: NextRequest) {
const url = request.nextUrl.toString();
const apis = paths.map(({ path, description }) => {
return { path: url + path, description };
});
return cors(
request,
new Response(JSON.stringify(apis), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,27 +0,0 @@
import cors from '@/lib/cors';
import { transformRepoStats } from '@/lib/transform-repo-stats';
export async function GET(request: Request) {
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
const data = await res.json();
const transformedData = transformRepoStats(data, 'forks');
return cors(
request,
new Response(JSON.stringify(transformedData), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,27 +0,0 @@
import cors from '@/lib/cors';
import { transformRepoStats } from '@/lib/transform-repo-stats';
export async function GET(request: Request) {
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
const data = await res.json();
const transformedData = transformRepoStats(data, 'openIssues');
return cors(
request,
new Response(JSON.stringify(transformedData), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,27 +0,0 @@
import cors from '@/lib/cors';
import { transformRepoStats } from '@/lib/transform-repo-stats';
export async function GET(request: Request) {
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
const data = await res.json();
const transformedData = transformRepoStats(data, 'mergedPRs');
return cors(
request,
new Response(JSON.stringify(transformedData), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,27 +0,0 @@
import cors from '@/lib/cors';
import { transformRepoStats } from '@/lib/transform-repo-stats';
export async function GET(request: Request) {
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
const data = await res.json();
const transformedData = transformRepoStats(data, 'stars');
return cors(
request,
new Response(JSON.stringify(transformedData), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,25 +0,0 @@
import cors from '@/lib/cors';
export async function GET(request: Request) {
const res = await fetch('https://api.github.com/repos/documenso/documenso');
const { forks_count } = await res.json();
return cors(
request,
new Response(JSON.stringify({ data: forks_count }), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,27 +0,0 @@
import cors from '@/lib/cors';
export async function GET(request: Request) {
const res = await fetch(
'https://api.github.com/search/issues?q=repo:documenso/documenso+type:issue+state:open&page=0&per_page=1',
);
const { total_count } = await res.json();
return cors(
request,
new Response(JSON.stringify({ data: total_count }), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,27 +0,0 @@
import cors from '@/lib/cors';
export async function GET(request: Request) {
const res = await fetch(
'https://api.github.com/search/issues?q=repo:documenso/documenso/+is:pr+merged:>=2010-01-01&page=0&per_page=1',
);
const { total_count } = await res.json();
return cors(
request,
new Response(JSON.stringify({ data: total_count }), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,36 +0,0 @@
import type { NextRequest } from 'next/server';
import cors from '@/lib/cors';
const paths = [
{ path: '/forks', description: 'GitHub Forks' },
{ path: '/stars', description: 'GitHub Stars' },
{ path: '/issues', description: 'GitHub Merged Issues' },
{ path: '/prs', description: 'GitHub Pull Request' },
];
export function GET(request: NextRequest) {
const url = request.nextUrl.toString();
const apis = paths.map(({ path, description }) => {
return { path: url + path, description };
});
return cors(
request,
new Response(JSON.stringify(apis), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,25 +0,0 @@
import cors from '@/lib/cors';
export async function GET(request: Request) {
const res = await fetch('https://api.github.com/repos/documenso/documenso');
const { stargazers_count } = await res.json();
return cors(
request,
new Response(JSON.stringify({ data: stargazers_count }), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,25 +0,0 @@
import cors from '@/lib/cors';
import { getUserMonthlyGrowth } from '@/lib/growth/get-user-monthly-growth';
export async function GET(request: Request) {
const monthlyUsers = await getUserMonthlyGrowth();
return cors(
request,
new Response(JSON.stringify(monthlyUsers), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,38 +0,0 @@
import type { NextRequest } from 'next/server';
import cors from '@/lib/cors';
const paths = [
{ path: '/total-customers', description: 'Total Customers' },
{ path: '/total-users', description: 'Total Users' },
{ path: '/new-users', description: 'New Users' },
{ path: '/completed-documents', description: 'Completed Documents per Month' },
{ path: '/total-completed-documents', description: 'Total Completed Documents' },
{ path: '/twitter', description: 'Twitter' },
];
export function GET(request: NextRequest) {
const url = request.nextUrl.toString();
const apis = paths.map(({ path, description }) => {
return { path: url + path, description };
});
return cors(
request,
new Response(JSON.stringify(apis), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,25 +0,0 @@
import cors from '@/lib/cors';
import { getUserMonthlyGrowth } from '@/lib/growth/get-user-monthly-growth';
export async function GET(request: Request) {
const totalUsers = await getUserMonthlyGrowth();
return cors(
request,
new Response(JSON.stringify(totalUsers), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,35 +0,0 @@
import type { NextRequest } from 'next/server';
import cors from '@/lib/cors';
const paths = [
{ path: 'github', description: 'GitHub Data' },
{ path: 'community', description: 'Community Data' },
{ path: 'growth', description: 'Growth Data' },
];
export function GET(request: NextRequest) {
const url = request.nextUrl.toString();
const apis = paths.map(({ path, description }) => {
return { path: url + path, description };
});
return cors(
request,
new Response(JSON.stringify(apis), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@@ -1,138 +0,0 @@
/**
* Multi purpose CORS lib.
* Note: Based on the `cors` package in npm but using only web APIs.
* Taken from: https://github.com/vercel/examples/blob/main/edge-functions/cors/lib/cors.ts
*/
type StaticOrigin = boolean | string | RegExp | (boolean | string | RegExp)[];
type OriginFn = (origin: string | undefined, req: Request) => StaticOrigin | Promise<StaticOrigin>;
interface CorsOptions {
origin?: StaticOrigin | OriginFn;
methods?: string | string[];
allowedHeaders?: string | string[];
exposedHeaders?: string | string[];
credentials?: boolean;
maxAge?: number;
preflightContinue?: boolean;
optionsSuccessStatus?: number;
}
const defaultOptions: CorsOptions = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
};
function isOriginAllowed(origin: string, allowed: StaticOrigin): boolean {
return Array.isArray(allowed)
? allowed.some((o) => isOriginAllowed(origin, o))
: typeof allowed === 'string'
? origin === allowed
: allowed instanceof RegExp
? allowed.test(origin)
: !!allowed;
}
function getOriginHeaders(reqOrigin: string | undefined, origin: StaticOrigin) {
const headers = new Headers();
if (origin === '*') {
// Allow any origin
headers.set('Access-Control-Allow-Origin', '*');
} else if (typeof origin === 'string') {
// Fixed origin
headers.set('Access-Control-Allow-Origin', origin);
headers.append('Vary', 'Origin');
} else {
const allowed = isOriginAllowed(reqOrigin ?? '', origin);
if (allowed && reqOrigin) {
headers.set('Access-Control-Allow-Origin', reqOrigin);
}
headers.append('Vary', 'Origin');
}
return headers;
}
async function originHeadersFromReq(req: Request, origin: StaticOrigin | OriginFn) {
const reqOrigin = req.headers.get('Origin') || undefined;
const value = typeof origin === 'function' ? await origin(reqOrigin, req) : origin;
if (!value) return;
return getOriginHeaders(reqOrigin, value);
}
function getAllowedHeaders(req: Request, allowed?: string | string[]) {
const headers = new Headers();
if (!allowed) {
allowed = req.headers.get('Access-Control-Request-Headers')!;
headers.append('Vary', 'Access-Control-Request-Headers');
} else if (Array.isArray(allowed)) {
// If the allowed headers is an array, turn it into a string
allowed = allowed.join(',');
}
if (allowed) {
headers.set('Access-Control-Allow-Headers', allowed);
}
return headers;
}
export default async function cors(req: Request, res: Response, options?: CorsOptions) {
const opts = { ...defaultOptions, ...options };
const { headers } = res;
const originHeaders = await originHeadersFromReq(req, opts.origin ?? false);
const mergeHeaders = (v: string, k: string) => {
if (k === 'Vary') headers.append(k, v);
else headers.set(k, v);
};
// If there's no origin we won't touch the response
if (!originHeaders) return res;
originHeaders.forEach(mergeHeaders);
if (opts.credentials) {
headers.set('Access-Control-Allow-Credentials', 'true');
}
const exposed = Array.isArray(opts.exposedHeaders)
? opts.exposedHeaders.join(',')
: opts.exposedHeaders;
if (exposed) {
headers.set('Access-Control-Expose-Headers', exposed);
}
// Handle the preflight request
if (req.method === 'OPTIONS') {
if (opts.methods) {
const methods = Array.isArray(opts.methods) ? opts.methods.join(',') : opts.methods;
headers.set('Access-Control-Allow-Methods', methods);
}
getAllowedHeaders(req, opts.allowedHeaders).forEach(mergeHeaders);
if (typeof opts.maxAge === 'number') {
headers.set('Access-Control-Max-Age', String(opts.maxAge));
}
if (opts.preflightContinue) return res;
headers.set('Content-Length', '0');
return new Response(null, { status: opts.optionsSuccessStatus, headers });
}
// If we got here, it's a normal request
return res;
}
export function initCors(options?: CorsOptions) {
return async (req: Request, res: Response) => cors(req, res, options);
}

View File

@@ -1,38 +0,0 @@
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count') => {
const qb = kyselyPrisma.$kysely
.selectFrom('User')
.select(({ fn }) => [
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']).as('month'),
fn.count('id').as('count'),
fn
.sum(fn.count('id'))
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']) as any))
.as('cume_count'),
])
.groupBy('month')
.orderBy('month', 'desc')
.limit(12);
const result = await qb.execute();
const transformedData = {
labels: result.map((row) => DateTime.fromJSDate(row.month).toFormat('MMM yyyy')).reverse(),
datasets: [
{
label: type === 'count' ? 'New Users' : 'Total Users',
data: result
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
.reverse(),
},
],
};
return transformedData;
};
export type GetUserMonthlyGrowthResult = Awaited<ReturnType<typeof getUserMonthlyGrowth>>;

View File

@@ -1,70 +0,0 @@
type RepoStats = {
stars: number;
forks: number;
mergedPRs: number;
openIssues: number;
};
type DataEntry = {
[key: string]: RepoStats;
};
type TransformedData = {
labels: string[];
datasets: {
label: string;
data: number[];
}[];
};
type MonthNames = {
[key: string]: string;
};
type MetricKey = keyof RepoStats;
const FRIENDLY_METRIC_NAMES: { [key in MetricKey]: string } = {
stars: 'Stars',
forks: 'Forks',
mergedPRs: 'Merged PRs',
openIssues: 'Open Issues',
};
export function transformRepoStats(data: DataEntry, metric: MetricKey): TransformedData {
const sortedEntries = Object.entries(data).sort(([dateA], [dateB]) => {
const [yearA, monthA] = dateA.split('-').map(Number);
const [yearB, monthB] = dateB.split('-').map(Number);
return new Date(yearA, monthA - 1).getTime() - new Date(yearB, monthB - 1).getTime();
});
const monthNames: MonthNames = {
'1': 'Jan',
'2': 'Feb',
'3': 'Mar',
'4': 'Apr',
'5': 'May',
'6': 'Jun',
'7': 'Jul',
'8': 'Aug',
'9': 'Sep',
'10': 'Oct',
'11': 'Nov',
'12': 'Dec',
};
const labels = sortedEntries.map(([date]) => {
const [year, month] = date.split('-');
const monthIndex = parseInt(month);
return `${monthNames[monthIndex.toString()]} ${year}`;
});
return {
labels,
datasets: [
{
label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`,
data: sortedEntries.map(([_, stats]) => stats[metric]),
},
],
};
}

View File

@@ -1,4 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = nextConfig;

View File

@@ -1,22 +0,0 @@
{
"name": "@documenso/openpage-api",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 3003",
"build": "next build",
"start": "next start",
"lint:fix": "next lint --fix",
"clean": "rimraf .next && rimraf node_modules",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
"@documenso/prisma": "*",
"next": "14.2.6"
},
"devDependencies": {
"@types/node": "20.16.5",
"@types/react": "18.3.5",
"typescript": "5.5.4"
}
}

View File

@@ -1,27 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -1,11 +1,11 @@
{ {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.7.2-rc.1", "version": "1.7.2-rc.4",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "next dev -p 3000", "dev": "next dev -p 3000",
"build": "turbo run translate:extract && turbo run translate:compile && next build", "build": "npm run translate:extract --prefix ../../ && turbo run translate:compile && next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"e2e:prepare": "next build && next start", "e2e:prepare": "next build && next start",

View File

@@ -24,7 +24,7 @@ type AdminDocumentDetailsPageProps = {
}; };
export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) { export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) {
const { i18n } = setupI18nSSR(); const { i18n } = await setupI18nSSR();
const document = await getEntireDocument({ id: Number(params.id) }); const document = await getEntireDocument({ id: Number(params.id) });

View File

@@ -4,8 +4,8 @@ import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { AdminDocumentResults } from './document-results'; import { AdminDocumentResults } from './document-results';
export default function AdminDocumentsPage() { export default async function AdminDocumentsPage() {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<div> <div>

View File

@@ -13,7 +13,7 @@ export type AdminSectionLayoutProps = {
}; };
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) { export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
setupI18nSSR(); await setupI18nSSR();
const { user } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();

View File

@@ -12,7 +12,7 @@ import { BannerForm } from './banner-form';
// import { BannerForm } from './banner-form'; // import { BannerForm } from './banner-form';
export default async function AdminBannerPage() { export default async function AdminBannerPage() {
setupI18nSSR(); await setupI18nSSR();
const { _ } = useLingui(); const { _ } = useLingui();

View File

@@ -30,7 +30,7 @@ import { SignerConversionChart } from './signer-conversion-chart';
import { UserWithDocumentChart } from './user-with-document'; import { UserWithDocumentChart } from './user-with-document';
export default async function AdminStatsPage() { export default async function AdminStatsPage() {
setupI18nSSR(); await setupI18nSSR();
const { _ } = useLingui(); const { _ } = useLingui();

View File

@@ -14,7 +14,7 @@ import {
} from '@documenso/ui/primitives/table'; } from '@documenso/ui/primitives/table';
export default async function Subscriptions() { export default async function Subscriptions() {
setupI18nSSR(); await setupI18nSSR();
const subscriptions = await findSubscriptions(); const subscriptions = await findSubscriptions();

View File

@@ -16,7 +16,7 @@ type AdminManageUsersProps = {
}; };
export default async function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) { export default async function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) {
setupI18nSSR(); await setupI18nSSR();
const page = Number(searchParams.page) || 1; const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 10; const perPage = Number(searchParams.perPage) || 10;

View File

@@ -7,6 +7,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
import { msg } from '@lingui/macro'; import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import { import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION, DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META, SKIP_QUERY_BATCH_META,
@@ -201,7 +202,7 @@ export const EditDocumentForm = ({
const onAddSettingsFormSubmit = async (data: TAddSettingsFormSchema) => { const onAddSettingsFormSubmit = async (data: TAddSettingsFormSchema) => {
try { try {
const { timezone, dateFormat, redirectUrl } = data.meta; const { timezone, dateFormat, redirectUrl, language } = data.meta;
await setSettingsForDocument({ await setSettingsForDocument({
documentId: document.id, documentId: document.id,
@@ -217,6 +218,7 @@ export const EditDocumentForm = ({
timezone, timezone,
dateFormat, dateFormat,
redirectUrl, redirectUrl,
language: isValidLanguageCode(language) ? language : undefined,
}, },
}); });

View File

@@ -8,8 +8,8 @@ export type DocumentPageProps = {
}; };
}; };
export default function DocumentEditPage({ params }: DocumentPageProps) { export default async function DocumentEditPage({ params }: DocumentPageProps) {
setupI18nSSR(); await setupI18nSSR();
return <DocumentEditPageView params={params} />; return <DocumentEditPageView params={params} />;
} }

View File

@@ -6,8 +6,8 @@ import { ChevronLeft, Loader } from 'lucide-react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
export default function Loading() { export default async function Loading() {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8"> <div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">

View File

@@ -8,8 +8,8 @@ export type DocumentsLogsPageProps = {
}; };
}; };
export default function DocumentsLogsPage({ params }: DocumentsLogsPageProps) { export default async function DocumentsLogsPage({ params }: DocumentsLogsPageProps) {
setupI18nSSR(); await setupI18nSSR();
return <DocumentLogsPageView params={params} />; return <DocumentLogsPageView params={params} />;
} }

View File

@@ -8,8 +8,8 @@ export type DocumentPageProps = {
}; };
}; };
export default function DocumentPage({ params }: DocumentPageProps) { export default async function DocumentPage({ params }: DocumentPageProps) {
setupI18nSSR(); await setupI18nSSR();
return <DocumentPageView params={params} />; return <DocumentPageView params={params} />;
} }

View File

@@ -5,8 +5,8 @@ import { ChevronLeft } from 'lucide-react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
export default function DocumentSentPage() { export default async function DocumentSentPage() {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8"> <div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">

View File

@@ -87,7 +87,7 @@ export const DeleteDocumentDialog = ({
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value); setInputValue(event.target.value);
setIsDeleteEnabled(event.target.value === 'delete'); setIsDeleteEnabled(event.target.value === _(msg`delete`));
}; };
return ( return (

View File

@@ -117,10 +117,10 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
<DialogFooter> <DialogFooter>
<Button variant="secondary" onClick={() => onOpenChange(false)}> <Button variant="secondary" onClick={() => onOpenChange(false)}>
Cancel <Trans>Cancel</Trans>
</Button> </Button>
<Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}> <Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}>
{isLoading ? 'Moving...' : 'Move'} {isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@@ -16,7 +16,7 @@ export const metadata: Metadata = {
}; };
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) { export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { user } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();

View File

@@ -23,7 +23,7 @@ export type AuthenticatedDashboardLayoutProps = {
export default async function AuthenticatedDashboardLayout({ export default async function AuthenticatedDashboardLayout({
children, children,
}: AuthenticatedDashboardLayoutProps) { }: AuthenticatedDashboardLayoutProps) {
setupI18nSSR(); await setupI18nSSR();
const session = await getServerSession(NEXT_AUTH_OPTIONS); const session = await getServerSession(NEXT_AUTH_OPTIONS);

View File

@@ -24,7 +24,7 @@ export const metadata: Metadata = {
}; };
export default async function BillingSettingsPage() { export default async function BillingSettingsPage() {
const { i18n } = setupI18nSSR(); const { i18n } = await setupI18nSSR();
let { user } = await getRequiredServerComponentSession(); let { user } = await getRequiredServerComponentSession();

View File

@@ -11,8 +11,8 @@ export type DashboardSettingsLayoutProps = {
children: React.ReactNode; children: React.ReactNode;
}; };
export default function DashboardSettingsLayout({ children }: DashboardSettingsLayoutProps) { export default async function DashboardSettingsLayout({ children }: DashboardSettingsLayoutProps) {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8"> <div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">

View File

@@ -17,7 +17,7 @@ export const metadata: Metadata = {
}; };
export default async function ProfileSettingsPage() { export default async function ProfileSettingsPage() {
setupI18nSSR(); await setupI18nSSR();
const { _ } = useLingui(); const { _ } = useLingui();
const { user } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();

View File

@@ -5,7 +5,7 @@ import { getUserPublicProfile } from '@documenso/lib/server-only/user/get-user-p
import { PublicProfilePageView } from './public-profile-page-view'; import { PublicProfilePageView } from './public-profile-page-view';
export default async function Page() { export default async function Page() {
setupI18nSSR(); await setupI18nSSR();
const { user } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();

View File

@@ -14,8 +14,8 @@ export const metadata: Metadata = {
title: 'Security activity', title: 'Security activity',
}; };
export default function SettingsSecurityActivityPage() { export default async function SettingsSecurityActivityPage() {
setupI18nSSR(); await setupI18nSSR();
const { _ } = useLingui(); const { _ } = useLingui();

View File

@@ -21,7 +21,7 @@ export const metadata: Metadata = {
}; };
export default async function SecuritySettingsPage() { export default async function SecuritySettingsPage() {
setupI18nSSR(); await setupI18nSSR();
const { _ } = useLingui(); const { _ } = useLingui();
const { user } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();

View File

@@ -17,7 +17,7 @@ export const metadata: Metadata = {
}; };
export default async function SettingsManagePasskeysPage() { export default async function SettingsManagePasskeysPage() {
setupI18nSSR(); await setupI18nSSR();
const { _ } = useLingui(); const { _ } = useLingui();
const isPasskeyEnabled = await getServerComponentFlag('app_passkey'); const isPasskeyEnabled = await getServerComponentFlag('app_passkey');

View File

@@ -10,7 +10,7 @@ import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-to
import { ApiTokenForm } from '~/components/forms/token'; import { ApiTokenForm } from '~/components/forms/token';
export default async function ApiTokensPage() { export default async function ApiTokensPage() {
const { i18n } = setupI18nSSR(); const { i18n } = await setupI18nSSR();
const { user } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();

View File

@@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation';
import { msg } from '@lingui/macro'; import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import { import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION, DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META, SKIP_QUERY_BATCH_META,
@@ -151,7 +152,10 @@ export const EditTemplateForm = ({
globalAccessAuth: data.globalAccessAuth ?? null, globalAccessAuth: data.globalAccessAuth ?? null,
globalActionAuth: data.globalActionAuth ?? null, globalActionAuth: data.globalActionAuth ?? null,
}, },
meta: data.meta, meta: {
...data.meta,
language: isValidLanguageCode(data.meta.language) ? data.meta.language : undefined,
},
}); });
// Router refresh is here to clear the router cache for when navigating to /documents. // Router refresh is here to clear the router cache for when navigating to /documents.

View File

@@ -7,8 +7,8 @@ import { TemplatePageView } from './template-page-view';
type TemplatePageProps = Pick<TemplatePageViewProps, 'params'>; type TemplatePageProps = Pick<TemplatePageViewProps, 'params'>;
export default function TemplatePage({ params }: TemplatePageProps) { export default async function TemplatePage({ params }: TemplatePageProps) {
setupI18nSSR(); await setupI18nSSR();
return <TemplatePageView params={params} />; return <TemplatePageView params={params} />;
} }

View File

@@ -15,8 +15,8 @@ export const metadata: Metadata = {
title: 'Templates', title: 'Templates',
}; };
export default function TemplatesPage({ searchParams = {} }: TemplatesPageProps) { export default async function TemplatesPage({ searchParams = {} }: TemplatesPageProps) {
setupI18nSSR(); await setupI18nSSR();
return <TemplatesPageView searchParams={searchParams} />; return <TemplatesPageView searchParams={searchParams} />;
} }

View File

@@ -14,7 +14,7 @@ type PublicProfileLayoutProps = {
}; };
export default async function PublicProfileLayout({ children }: PublicProfileLayoutProps) { export default async function PublicProfileLayout({ children }: PublicProfileLayoutProps) {
setupI18nSSR(); await setupI18nSSR();
const { user, session } = await getServerComponentSession(); const { user, session } = await getServerComponentSession();

View File

@@ -42,7 +42,7 @@ const BADGE_DATA = {
}; };
export default async function PublicProfilePage({ params }: PublicProfilePageProps) { export default async function PublicProfilePage({ params }: PublicProfilePageProps) {
setupI18nSSR(); await setupI18nSSR();
const { url: profileUrl } = params; const { url: profileUrl } = params;

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -77,7 +77,7 @@ export const ConfigureDirectTemplateFormPartial = ({
if (template.Recipient.map((recipient) => recipient.email).includes(items.email)) { if (template.Recipient.map((recipient) => recipient.email).includes(items.email)) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: 'Email cannot already exist in the template', message: _(msg`Email cannot already exist in the template`),
path: ['email'], path: ['email'],
}); });
} }

View File

@@ -24,7 +24,7 @@ export type TemplatesDirectPageProps = {
}; };
export default async function TemplatesDirectPage({ params }: TemplatesDirectPageProps) { export default async function TemplatesDirectPage({ params }: TemplatesDirectPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { token } = params; const { token } = params;

View File

@@ -19,7 +19,7 @@ type RecipientLayoutProps = {
* Such as direct template access, or signing. * Such as direct template access, or signing.
*/ */
export default async function RecipientLayout({ children }: RecipientLayoutProps) { export default async function RecipientLayout({ children }: RecipientLayoutProps) {
setupI18nSSR(); await setupI18nSSR();
const { user, session } = await getServerComponentSession(); const { user, session } = await getServerComponentSession();

View File

@@ -8,8 +8,8 @@ export type SigningLayoutProps = {
children: React.ReactNode; children: React.ReactNode;
}; };
export default function SigningLayout({ children }: SigningLayoutProps) { export default async function SigningLayout({ children }: SigningLayoutProps) {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<div> <div>

View File

@@ -40,7 +40,7 @@ export type CompletedSigningPageProps = {
export default async function CompletedSigningPage({ export default async function CompletedSigningPage({
params: { token }, params: { token },
}: CompletedSigningPageProps) { }: CompletedSigningPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { _ } = useLingui(); const { _ } = useLingui();
@@ -222,7 +222,7 @@ export default async function CompletedSigningPage({
)} )}
{isLoggedIn && ( {isLoggedIn && (
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600"> <Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-2">
<Trans>Go Back Home</Trans> <Trans>Go Back Home</Trans>
</Link> </Link>
)} )}

View File

@@ -124,9 +124,9 @@ export const SigningForm = ({
> >
<div className={cn('flex flex-1 flex-col')}> <div className={cn('flex flex-1 flex-col')}>
<h3 className="text-foreground text-2xl font-semibold"> <h3 className="text-foreground text-2xl font-semibold">
{recipient.role === RecipientRole.VIEWER && 'View Document'} {recipient.role === RecipientRole.VIEWER && <Trans>View Document</Trans>}
{recipient.role === RecipientRole.SIGNER && 'Sign Document'} {recipient.role === RecipientRole.SIGNER && <Trans>Sign Document</Trans>}
{recipient.role === RecipientRole.APPROVER && 'Approve Document'} {recipient.role === RecipientRole.APPROVER && <Trans>Approve Document</Trans>}
</h3> </h3>
{recipient.role === RecipientRole.VIEWER ? ( {recipient.role === RecipientRole.VIEWER ? (
@@ -166,7 +166,7 @@ export const SigningForm = ({
) : ( ) : (
<> <>
<p className="text-muted-foreground mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-sm">
Please review the document before signing. <Trans>Please review the document before signing.</Trans>
</p> </p>
<hr className="border-border mb-8 mt-4" /> <hr className="border-border mb-8 mt-4" />
@@ -174,7 +174,9 @@ export const SigningForm = ({
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2"> <div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
<div className="flex flex-1 flex-col gap-y-4"> <div className="flex flex-1 flex-col gap-y-4">
<div> <div>
<Label htmlFor="full-name">Full Name</Label> <Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
<Input <Input
type="text" type="text"
@@ -186,7 +188,9 @@ export const SigningForm = ({
</div> </div>
<div> <div>
<Label htmlFor="Signature">Signature</Label> <Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}> <Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0"> <CardContent className="p-0">
@@ -213,7 +217,7 @@ export const SigningForm = ({
disabled={typeof window !== 'undefined' && window.history.length <= 1} disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => router.back()} onClick={() => router.back()}
> >
Cancel <Trans>Cancel</Trans>
</Button> </Button>
<SignDialog <SignDialog

View File

@@ -4,6 +4,8 @@ import { useTransition } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@@ -37,6 +39,7 @@ export const InitialsField = ({
}: InitialsFieldProps) => { }: InitialsFieldProps) => {
const router = useRouter(); const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui();
const { fullName } = useRequiredSigningContext(); const { fullName } = useRequiredSigningContext();
const initials = extractInitials(fullName); const initials = extractInitials(fullName);
@@ -83,8 +86,8 @@ export const InitialsField = ({
console.error(err); console.error(err);
toast({ toast({
title: 'Error', title: _(msg`Error`),
description: 'An error occurred while signing the document.', description: _(msg`An error occurred while signing the document.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
@@ -109,8 +112,8 @@ export const InitialsField = ({
console.error(err); console.error(err);
toast({ toast({
title: 'Error', title: _(msg`Error`),
description: 'An error occurred while removing the signature.', description: _(msg`An error occurred while removing the field.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
@@ -126,7 +129,7 @@ export const InitialsField = ({
{!field.inserted && ( {!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground duration-200 group-hover:text-yellow-300"> <p className="group-hover:text-primary text-muted-foreground duration-200 group-hover:text-yellow-300">
Initials <Trans>Initials</Trans>
</p> </p>
)} )}

View File

@@ -13,7 +13,7 @@ export type SigningLayoutProps = {
}; };
export default async function SigningLayout({ children }: SigningLayoutProps) { export default async function SigningLayout({ children }: SigningLayoutProps) {
setupI18nSSR(); await setupI18nSSR();
const { user, session } = await getServerComponentSession(); const { user, session } = await getServerComponentSession();

View File

@@ -31,7 +31,7 @@ export type SigningPageProps = {
}; };
export default async function SigningPage({ params: { token } }: SigningPageProps) { export default async function SigningPage({ params: { token } }: SigningPageProps) {
setupI18nSSR(); await setupI18nSSR();
if (!token) { if (!token) {
return notFound(); return notFound();
@@ -43,12 +43,6 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders); const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
if (!isRecipientsTurn) {
return redirect(`/sign/${token}/waiting`);
}
const [document, fields, recipient, completedFields] = await Promise.all([ const [document, fields, recipient, completedFields] = await Promise.all([
getDocumentAndSenderByToken({ getDocumentAndSenderByToken({
token, token,
@@ -69,6 +63,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
return notFound(); return notFound();
} }
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
if (!isRecipientsTurn) {
return redirect(`/sign/${token}/waiting`);
}
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({ const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: document.authOptions, documentAuth: document.authOptions,
recipientAuth: recipient.authOptions, recipientAuth: recipient.authOptions,

View File

@@ -21,7 +21,7 @@ type WaitingForTurnToSignPageProps = {
export default async function WaitingForTurnToSignPage({ export default async function WaitingForTurnToSignPage({
params: { token }, params: { token },
}: WaitingForTurnToSignPageProps) { }: WaitingForTurnToSignPageProps) {
setupI18nSSR(); await setupI18nSSR();
if (!token) { if (!token) {
return notFound(); return notFound();

View File

@@ -12,7 +12,7 @@ export type DocumentPageProps = {
}; };
export default async function TeamsDocumentEditPage({ params }: DocumentPageProps) { export default async function TeamsDocumentEditPage({ params }: DocumentPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { teamUrl } = params; const { teamUrl } = params;

View File

@@ -12,7 +12,7 @@ export type TeamDocumentsLogsPageProps = {
}; };
export default async function TeamsDocumentsLogsPage({ params }: TeamDocumentsLogsPageProps) { export default async function TeamsDocumentsLogsPage({ params }: TeamDocumentsLogsPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { teamUrl } = params; const { teamUrl } = params;

View File

@@ -12,7 +12,7 @@ export type DocumentPageProps = {
}; };
export default async function DocumentPage({ params }: DocumentPageProps) { export default async function DocumentPage({ params }: DocumentPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { teamUrl } = params; const { teamUrl } = params;

View File

@@ -16,7 +16,7 @@ export default async function TeamsDocumentPage({
params, params,
searchParams = {}, searchParams = {},
}: TeamsDocumentPageProps) { }: TeamsDocumentPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { teamUrl } = params; const { teamUrl } = params;

View File

@@ -27,7 +27,7 @@ export default async function AuthenticatedTeamsLayout({
children, children,
params, params,
}: AuthenticatedTeamsLayoutProps) { }: AuthenticatedTeamsLayoutProps) {
setupI18nSSR(); await setupI18nSSR();
const { session, user } = await getServerComponentSession(); const { session, user } = await getServerComponentSession();

View File

@@ -21,7 +21,7 @@ export type TeamsSettingsBillingPageProps = {
}; };
export default async function TeamsSettingBillingPage({ params }: TeamsSettingsBillingPageProps) { export default async function TeamsSettingBillingPage({ params }: TeamsSettingsBillingPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { _ } = useLingui(); const { _ } = useLingui();

View File

@@ -24,7 +24,7 @@ export default async function TeamsSettingsLayout({
children, children,
params: { teamUrl }, params: { teamUrl },
}: TeamSettingsLayoutProps) { }: TeamSettingsLayoutProps) {
setupI18nSSR(); await setupI18nSSR();
const session = await getRequiredServerComponentSession(); const session = await getRequiredServerComponentSession();

View File

@@ -16,7 +16,7 @@ export type TeamsSettingsMembersPageProps = {
}; };
export default async function TeamsSettingsMembersPage({ params }: TeamsSettingsMembersPageProps) { export default async function TeamsSettingsMembersPage({ params }: TeamsSettingsMembersPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { _ } = useLingui(); const { _ } = useLingui();
const { teamUrl } = params; const { teamUrl } = params;

View File

@@ -28,7 +28,7 @@ export type TeamsSettingsPageProps = {
}; };
export default async function TeamsSettingsPage({ params }: TeamsSettingsPageProps) { export default async function TeamsSettingsPage({ params }: TeamsSettingsPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { teamUrl } = params; const { teamUrl } = params;

View File

@@ -14,7 +14,7 @@ export type TeamsSettingsPublicProfilePageProps = {
export default async function TeamsSettingsPublicProfilePage({ export default async function TeamsSettingsPublicProfilePage({
params, params,
}: TeamsSettingsPublicProfilePageProps) { }: TeamsSettingsPublicProfilePageProps) {
setupI18nSSR(); await setupI18nSSR();
const { teamUrl } = params; const { teamUrl } = params;

View File

@@ -21,7 +21,7 @@ type ApiTokensPageProps = {
}; };
export default async function ApiTokensPage({ params }: ApiTokensPageProps) { export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
const { i18n } = setupI18nSSR(); const { i18n } = await setupI18nSSR();
const { teamUrl } = params; const { teamUrl } = params;
@@ -97,17 +97,11 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
<h5 className="text-base">{token.name}</h5> <h5 className="text-base">{token.name}</h5>
<p className="text-muted-foreground mt-2 text-xs"> <p className="text-muted-foreground mt-2 text-xs">
<Trans> <Trans>Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)}</Trans>
Created on
{i18n.date(token.createdAt, DateTime.DATETIME_FULL)}
</Trans>
</p> </p>
{token.expires ? ( {token.expires ? (
<p className="text-muted-foreground mt-1 text-xs"> <p className="text-muted-foreground mt-1 text-xs">
<Trans> <Trans>Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)}</Trans>
Expires on
{i18n.date(token.expires, DateTime.DATETIME_FULL)}
</Trans>
</p> </p>
) : ( ) : (
<p className="text-muted-foreground mt-1 text-xs"> <p className="text-muted-foreground mt-1 text-xs">

View File

@@ -14,7 +14,7 @@ type TeamTemplatePageProps = {
}; };
export default async function TeamTemplatePage({ params }: TeamTemplatePageProps) { export default async function TeamTemplatePage({ params }: TeamTemplatePageProps) {
setupI18nSSR(); await setupI18nSSR();
const { teamUrl } = params; const { teamUrl } = params;

View File

@@ -18,7 +18,7 @@ export default async function TeamTemplatesPage({
searchParams = {}, searchParams = {},
params, params,
}: TeamTemplatesPageProps) { }: TeamTemplatesPageProps) {
setupI18nSSR(); await setupI18nSSR();
const { teamUrl } = params; const { teamUrl } = params;

View File

@@ -5,101 +5,156 @@ import { Trans } from '@lingui/macro';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
export default function SignatureDisclosure() { const SUPPORT_EMAIL = 'support@documenso.com';
setupI18nSSR();
export default async function SignatureDisclosure() {
await setupI18nSSR();
return ( return (
<div> <div>
<article className="prose dark:prose-invert"> <article className="prose dark:prose-invert">
<h1>Electronic Signature Disclosure</h1> <h1>
<Trans>Electronic Signature Disclosure</Trans>
</h1>
<h2>Welcome</h2> <h2>
<Trans>Welcome</Trans>
</h2>
<p> <p>
Thank you for using Documenso to perform your electronic document signing. The purpose of <Trans>
this disclosure is to inform you about the process, legality, and your rights regarding Thank you for using Documenso to perform your electronic document signing. The purpose
the use of electronic signatures on our platform. By opting to use an electronic of this disclosure is to inform you about the process, legality, and your rights
signature, you are agreeing to the terms and conditions outlined below. regarding the use of electronic signatures on our platform. By opting to use an
electronic signature, you are agreeing to the terms and conditions outlined below.
</Trans>
</p> </p>
<h2>Acceptance and Consent</h2> <h2>
<Trans>Acceptance and Consent</Trans>
</h2>
<p> <p>
When you use our platform to affix your electronic signature to documents, you are <Trans>
consenting to do so under the Electronic Signatures in Global and National Commerce Act When you use our platform to affix your electronic signature to documents, you are
(E-Sign Act) and other applicable laws. This action indicates your agreement to use consenting to do so under the Electronic Signatures in Global and National Commerce Act
electronic means to sign documents and receive notifications. (E-Sign Act) and other applicable laws. This action indicates your agreement to use
electronic means to sign documents and receive notifications.
</Trans>
</p> </p>
<h2>Legality of Electronic Signatures</h2> <h2>
<Trans>Legality of Electronic Signatures</Trans>
</h2>
<p> <p>
An electronic signature provided by you on our platform, achieved through clicking through <Trans>
to a document and entering your name, or any other electronic signing method we provide, An electronic signature provided by you on our platform, achieved through clicking
is legally binding. It carries the same weight and enforceability as a manual signature through to a document and entering your name, or any other electronic signing method we
written with ink on paper. provide, is legally binding. It carries the same weight and enforceability as a manual
signature written with ink on paper.
</Trans>
</p> </p>
<h2>System Requirements</h2> <h2>
<p>To use our electronic signature service, you must have access to:</p> <Trans>System Requirements</Trans>
</h2>
<p>
<Trans>To use our electronic signature service, you must have access to:</Trans>
</p>
<ul> <ul>
<li>A stable internet connection</li> <li>
<li>An email account</li> <Trans>A stable internet connection</Trans>
<li>A device capable of accessing, opening, and reading documents</li> </li>
<li>A means to print or download documents for your records</li> <li>
<Trans>An email account</Trans>
</li>
<li>
<Trans>A device capable of accessing, opening, and reading documents</Trans>
</li>
<li>
<Trans>A means to print or download documents for your records</Trans>
</li>
</ul> </ul>
<h2>Electronic Delivery of Documents</h2> <h2>
<Trans>Electronic Delivery of Documents</Trans>
</h2>
<p> <p>
All documents related to the electronic signing process will be provided to you <Trans>
electronically through our platform or via email. It is your responsibility to ensure that All documents related to the electronic signing process will be provided to you
your email address is current and that you can receive and open our emails. electronically through our platform or via email. It is your responsibility to ensure
that your email address is current and that you can receive and open our emails.
</Trans>
</p> </p>
<h2>Consent to Electronic Transactions</h2> <h2>
<Trans>Consent to Electronic Transactions</Trans>
</h2>
<p> <p>
By using the electronic signature feature, you are consenting to conduct transactions and <Trans>
receive disclosures electronically. You acknowledge that your electronic signature on By using the electronic signature feature, you are consenting to conduct transactions
documents is binding and that you accept the terms outlined in the documents you are and receive disclosures electronically. You acknowledge that your electronic signature
signing. on documents is binding and that you accept the terms outlined in the documents you are
signing.
</Trans>
</p> </p>
<h2>Withdrawing Consent</h2> <h2>
<Trans>Withdrawing Consent</Trans>
</h2>
<p> <p>
You have the right to withdraw your consent to use electronic signatures at any time <Trans>
before completing the signing process. To withdraw your consent, please contact the sender You have the right to withdraw your consent to use electronic signatures at any time
of the document. In failing to contact the sender you may reach out to{' '} before completing the signing process. To withdraw your consent, please contact the
<a href="mailto:support@documenso.com">support@documenso.com</a> for assistance. Be aware sender of the document. In failing to contact the sender you may reach out to{' '}
that withdrawing consent may delay or halt the completion of the related transaction or <a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a> for assistance. Be aware that
service. withdrawing consent may delay or halt the completion of the related transaction or
service.
</Trans>
</p> </p>
<h2>Updating Your Information</h2> <h2>
<Trans>Updating Your Information</Trans>
</h2>
<p> <p>
It is crucial to keep your contact information, especially your email address, up to date <Trans>
with us. Please notify us immediately of any changes to ensure that you continue to It is crucial to keep your contact information, especially your email address, up to
receive all necessary communications. date with us. Please notify us immediately of any changes to ensure that you continue to
receive all necessary communications.
</Trans>
</p> </p>
<h2>Retention of Documents</h2> <h2>
<Trans>Retention of Documents</Trans>
</h2>
<p> <p>
After signing a document electronically, you will be provided the opportunity to view, <Trans>
download, and print the document for your records. It is highly recommended that you After signing a document electronically, you will be provided the opportunity to view,
retain a copy of all electronically signed documents for your personal records. We will download, and print the document for your records. It is highly recommended that you
also retain a copy of the signed document for our records however we may not be able to retain a copy of all electronically signed documents for your personal records. We will
provide you with a copy of the signed document after a certain period of time. also retain a copy of the signed document for our records however we may not be able to
provide you with a copy of the signed document after a certain period of time.
</Trans>
</p> </p>
<h2>Acknowledgment</h2> <h2>
<Trans>Acknowledgment</Trans>
</h2>
<p> <p>
By proceeding to use the electronic signature service provided by Documenso, you affirm <Trans>
that you have read and understood this disclosure. You agree to all terms and conditions By proceeding to use the electronic signature service provided by Documenso, you affirm
related to the use of electronic signatures and electronic transactions as outlined that you have read and understood this disclosure. You agree to all terms and conditions
herein. related to the use of electronic signatures and electronic transactions as outlined
herein.
</Trans>
</p> </p>
<h2>Contact Information</h2> <h2>
<Trans>Contact Information</Trans>
</h2>
<p> <p>
For any questions regarding this disclosure, electronic signatures, or any related <Trans>
process, please contact us at:{' '} For any questions regarding this disclosure, electronic signatures, or any related
<a href="mailto:support@documenso.com">support@documenso.com</a> process, please contact us at: <a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a>
</Trans>
</p> </p>
</article> </article>

View File

@@ -10,8 +10,8 @@ export const metadata: Metadata = {
title: 'Forgot password', title: 'Forgot password',
}; };
export default function ForgotPasswordPage() { export default async function ForgotPasswordPage() {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<div className="w-screen max-w-lg px-4"> <div className="w-screen max-w-lg px-4">

View File

@@ -11,8 +11,8 @@ export const metadata: Metadata = {
title: 'Forgot Password', title: 'Forgot Password',
}; };
export default function ForgotPasswordPage() { export default async function ForgotPasswordPage() {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<div className="w-screen max-w-lg px-4"> <div className="w-screen max-w-lg px-4">

View File

@@ -9,8 +9,8 @@ type UnauthenticatedLayoutProps = {
children: React.ReactNode; children: React.ReactNode;
}; };
export default function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) { export default async function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<main className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24"> <main className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">

View File

@@ -15,7 +15,7 @@ type ResetPasswordPageProps = {
}; };
export default async function ResetPasswordPage({ params: { token } }: ResetPasswordPageProps) { export default async function ResetPasswordPage({ params: { token } }: ResetPasswordPageProps) {
setupI18nSSR(); await setupI18nSSR();
const isValid = await getResetTokenValidity({ token }); const isValid = await getResetTokenValidity({ token });

View File

@@ -10,8 +10,8 @@ export const metadata: Metadata = {
title: 'Reset Password', title: 'Reset Password',
}; };
export default function ResetPasswordPage() { export default async function ResetPasswordPage() {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<div className="w-screen max-w-lg px-4"> <div className="w-screen max-w-lg px-4">

View File

@@ -17,8 +17,8 @@ export const metadata: Metadata = {
title: 'Sign In', title: 'Sign In',
}; };
export default function SignInPage() { export default async function SignInPage() {
setupI18nSSR(); await setupI18nSSR();
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP'); const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');

View File

@@ -12,8 +12,8 @@ export const metadata: Metadata = {
title: 'Sign Up', title: 'Sign Up',
}; };
export default function SignUpPage() { export default async function SignUpPage() {
setupI18nSSR(); await setupI18nSSR();
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP'); const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');

View File

@@ -21,7 +21,7 @@ type DeclineInvitationPageProps = {
export default async function DeclineInvitationPage({ export default async function DeclineInvitationPage({
params: { token }, params: { token },
}: DeclineInvitationPageProps) { }: DeclineInvitationPageProps) {
setupI18nSSR(); await setupI18nSSR();
const session = await getServerComponentSession(); const session = await getServerComponentSession();

View File

@@ -21,7 +21,7 @@ type AcceptInvitationPageProps = {
export default async function AcceptInvitationPage({ export default async function AcceptInvitationPage({
params: { token }, params: { token },
}: AcceptInvitationPageProps) { }: AcceptInvitationPageProps) {
setupI18nSSR(); await setupI18nSSR();
const session = await getServerComponentSession(); const session = await getServerComponentSession();

View File

@@ -14,7 +14,7 @@ type VerifyTeamEmailPageProps = {
}; };
export default async function VerifyTeamEmailPage({ params: { token } }: VerifyTeamEmailPageProps) { export default async function VerifyTeamEmailPage({ params: { token } }: VerifyTeamEmailPageProps) {
setupI18nSSR(); await setupI18nSSR();
const teamEmailVerification = await prisma.teamEmailVerification.findUnique({ const teamEmailVerification = await prisma.teamEmailVerification.findUnique({
where: { where: {

View File

@@ -17,7 +17,7 @@ type VerifyTeamTransferPage = {
export default async function VerifyTeamTransferPage({ export default async function VerifyTeamTransferPage({
params: { token }, params: { token },
}: VerifyTeamTransferPage) { }: VerifyTeamTransferPage) {
setupI18nSSR(); await setupI18nSSR();
const teamTransferVerification = await prisma.teamTransferVerification.findUnique({ const teamTransferVerification = await prisma.teamTransferVerification.findUnique({
where: { where: {

View File

@@ -5,8 +5,8 @@ import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { SendConfirmationEmailForm } from '~/components/forms/send-confirmation-email'; import { SendConfirmationEmailForm } from '~/components/forms/send-confirmation-email';
export default function UnverifiedAccount() { export default async function UnverifiedAccount() {
setupI18nSSR(); await setupI18nSSR();
return ( return (
<div className="w-screen max-w-lg px-4"> <div className="w-screen max-w-lg px-4">

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