mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 07:43:16 +10:00
feat: initial i18n marketing implementation (#1223)
## Description This PR introduces an initial i18n implementation using [Lingui](https://lingui.dev). We plan to combine it with Crowdin which will provide AI translations when PRs are merged into main. We plan to rollout i18n to only marketing for now, and will review how everything goes before continuing to introduce it into the main application. ## Reasoning Why not use i18n-next or other alternatives? To hopefully provide the best DX we chose Lingui because it allows us to simply wrap text that we want to translate in tags, instead of forcing users to do things such as: - Update the text to `t('some-text')` - Extract it to the file - The text becomes a bit unreadable unless done correctly Yes, plugins such as i18n-ally and Sherlock exist to simplify these chores, but these require the user to be correctly setup in vscode, and it also does not seem to provide the required configurations for our multi app and multi UI package setup. ## Super simple demo ```html // Before <p>Text to update</p> // After <p> <Trans>Text to update</Trans> </p> ``` ## Related Issue Relates to #885 but is only for marketing for now. Another branch is slowly being prepared for the changes required for the web application while we wait to see how this goes for marketing. ## Changes Made Our configuration does not follow the general standard since we have translations that cross: - Web app - Marketing app - Constants package - UI package This means we want to separate translations into: 1. Marketing - Only translations extracted from `apps/marketing` 2. Web - Only translations extracted from `apps/web` 3. Common - Translations from `packages/constants` and `packages/ui` Then we bundle, compile and minify the translations for production as follows: 1. Marketing = Marketing + Common 2. Web = Web + Common This allows us to only load the required translations when running each application. Overall general changes: - Add i18n to marketing - Add core i18n setup to web - Add pre-commit hook and GH action to extract any new <Trans> tags into the translation files <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added Romanian localization for marketing messages to improve accessibility for Romanian-speaking users. - Introduced German and English translation modules and PO files to enhance the application's internationalization capabilities. - Integrated internationalization support in the RootLayout component for dynamic language settings based on server-side configurations. - Enhanced the Enterprise component with translation support to adapt to user language preferences. - Added a `<meta>` tag to prevent Google from translating the page content, supporting internationalization efforts. - **Bug Fixes** - Resolved minor issues related to the structure and accessibility of translation files. - **Chores** - Updated project dependencies to support the new localization features and ensure stability. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Lucas Smith <me@lucasjamessmith.me> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: github-actions <github-actions@documenso.com>
This commit is contained in:
22
apps/marketing/lingui.config.ts
Normal file
22
apps/marketing/lingui.config.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { LinguiConfig } from '@lingui/conf';
|
||||
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
|
||||
// Extends root lingui.config.cjs.
|
||||
const config: LinguiConfig = {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
locales: APP_I18N_OPTIONS.supportedLangs as unknown as string[],
|
||||
catalogs: [
|
||||
{
|
||||
path: '<rootDir>/../../packages/lib/translations/{locale}/marketing',
|
||||
include: ['<rootDir>/apps/marketing/src'],
|
||||
},
|
||||
{
|
||||
path: '<rootDir>/../../packages/lib/translations/{locale}/common',
|
||||
include: ['<rootDir>/packages/ui', '<rootDir>/packages/lib'],
|
||||
},
|
||||
],
|
||||
catalogsMergePath: '<rootDir>/../../packages/lib/translations/{locale}/marketing',
|
||||
};
|
||||
|
||||
export default config;
|
||||
@ -30,6 +30,7 @@ const config = {
|
||||
serverActions: {
|
||||
bodySizeLimit: '50mb',
|
||||
},
|
||||
swcPlugins: [['@lingui/swc-plugin', {}]],
|
||||
},
|
||||
reactStrictMode: true,
|
||||
transpilePackages: [
|
||||
@ -55,6 +56,13 @@ const config = {
|
||||
config.resolve.alias.canvas = false;
|
||||
}
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.po$/,
|
||||
use: {
|
||||
loader: '@lingui/loader',
|
||||
},
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
async headers() {
|
||||
|
||||
@ -10,7 +10,8 @@
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"clean": "rimraf .next && rimraf node_modules",
|
||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs",
|
||||
"translate:compile": "lingui compile --typescript"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/assets": "*",
|
||||
@ -19,7 +20,10 @@
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@lingui/macro": "^4.11.1",
|
||||
"@lingui/react": "^4.11.1",
|
||||
"@openstatus/react": "^0.0.3",
|
||||
"cmdk": "^0.2.1",
|
||||
"contentlayer": "^0.3.4",
|
||||
"embla-carousel": "^8.1.3",
|
||||
"embla-carousel-autoplay": "^8.1.3",
|
||||
@ -46,6 +50,8 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/loader": "^4.11.1",
|
||||
"@lingui/swc-plugin": "4.0.6",
|
||||
"@types/node": "20.1.0",
|
||||
"@types/react": "18.2.18",
|
||||
"@types/react-dom": "18.2.7"
|
||||
@ -58,4 +64,4 @@
|
||||
"next": "$next"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,17 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { allBlogPosts } from 'contentlayer/generated';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog',
|
||||
};
|
||||
|
||||
export default function BlogPage() {
|
||||
const { i18n } = setupI18nSSR();
|
||||
|
||||
const blogPosts = allBlogPosts.sort((a, b) => {
|
||||
const dateA = new Date(a.date);
|
||||
const dateB = new Date(b.date);
|
||||
@ -17,11 +22,15 @@ export default function BlogPage() {
|
||||
return (
|
||||
<div className="mt-6 sm:mt-12">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">From the blog</h1>
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">
|
||||
<Trans>From the blog</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mx-auto mt-4 max-w-xl text-center text-lg leading-normal">
|
||||
Get the latest news from Documenso, including product updates, team announcements and
|
||||
more!
|
||||
<Trans>
|
||||
Get the latest news from Documenso, including product updates, team announcements and
|
||||
more!
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -33,7 +42,7 @@ export default function BlogPage() {
|
||||
>
|
||||
<div className="flex items-center gap-x-4 text-xs">
|
||||
<time dateTime={post.date} className="text-muted-foreground">
|
||||
{new Date(post.date).toLocaleDateString()}
|
||||
<Trans>{i18n.date(new Date(), { dateStyle: 'short' })}</Trans>
|
||||
</time>
|
||||
|
||||
{post.tags.length > 0 && (
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
export const TEAM_MEMBERS = [
|
||||
{
|
||||
name: 'Timur Ercan',
|
||||
role: 'Co-Founder, CEO',
|
||||
salary: 95_000,
|
||||
location: 'Germany',
|
||||
engagement: 'Full-Time',
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'November 14th, 2022',
|
||||
},
|
||||
{
|
||||
@ -12,7 +14,7 @@ export const TEAM_MEMBERS = [
|
||||
role: 'Co-Founder, CTO',
|
||||
salary: 95_000,
|
||||
location: 'Australia',
|
||||
engagement: 'Full-Time',
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'April 19th, 2023',
|
||||
},
|
||||
{
|
||||
@ -20,7 +22,7 @@ export const TEAM_MEMBERS = [
|
||||
role: 'Software Engineer - Intern',
|
||||
salary: 15_000,
|
||||
location: 'Ghana',
|
||||
engagement: 'Part-Time',
|
||||
engagement: msg`Part-Time`,
|
||||
joinDate: 'June 6th, 2023',
|
||||
},
|
||||
{
|
||||
@ -28,7 +30,7 @@ export const TEAM_MEMBERS = [
|
||||
role: 'Software Engineer - III',
|
||||
salary: 100_000,
|
||||
location: 'Australia',
|
||||
engagement: 'Full-Time',
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'July 26th, 2023',
|
||||
},
|
||||
{
|
||||
@ -36,7 +38,7 @@ export const TEAM_MEMBERS = [
|
||||
role: 'Software Engineer - II',
|
||||
salary: 80_000,
|
||||
location: 'Romania',
|
||||
engagement: 'Full-Time',
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'September 4th, 2023',
|
||||
},
|
||||
{
|
||||
@ -44,7 +46,7 @@ export const TEAM_MEMBERS = [
|
||||
role: 'Designer - III',
|
||||
salary: 100_000,
|
||||
location: 'India',
|
||||
engagement: 'Full-Time',
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'October 9th, 2023',
|
||||
},
|
||||
];
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
||||
@ -11,6 +13,8 @@ export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
|
||||
};
|
||||
|
||||
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const formattedData = data.map((item) => ({
|
||||
amount: Number(item.amount),
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
@ -21,7 +25,9 @@ export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps)
|
||||
<div className={className} {...props}>
|
||||
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">Total Funding Raised</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>Total Funding Raised</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@ -49,14 +55,14 @@ export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps)
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 0,
|
||||
}),
|
||||
'Amount Raised',
|
||||
_(msg`Amount Raised`),
|
||||
]}
|
||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="amount"
|
||||
fill="hsl(var(--primary))"
|
||||
label="Amount Raised"
|
||||
label={_(msg`Amount Raised`)}
|
||||
maxBarSize={60}
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
@ -14,6 +16,8 @@ export const MonthlyCompletedDocumentsChart = ({
|
||||
className,
|
||||
data,
|
||||
}: MonthlyCompletedDocumentsChartProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const formattedData = [...data].reverse().map(({ month, count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
|
||||
@ -25,7 +29,9 @@ export const MonthlyCompletedDocumentsChart = ({
|
||||
<div className={className}>
|
||||
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">Completed Documents per Month</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>Completed Documents per Month</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@ -46,7 +52,7 @@ export const MonthlyCompletedDocumentsChart = ({
|
||||
fill="hsl(var(--primary))"
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={60}
|
||||
label="Completed Documents"
|
||||
label={_(msg`Completed Documents`)}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
@ -11,6 +13,8 @@ export type MonthlyNewUsersChartProps = {
|
||||
};
|
||||
|
||||
export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const formattedData = [...data].reverse().map(({ month, count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
|
||||
@ -22,7 +26,9 @@ export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartPr
|
||||
<div className={className}>
|
||||
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">New Users</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>New Users</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@ -34,7 +40,7 @@ export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartPr
|
||||
labelStyle={{
|
||||
color: 'hsl(var(--primary-foreground))',
|
||||
}}
|
||||
formatter={(value) => [Number(value).toLocaleString('en-US'), 'New Users']}
|
||||
formatter={(value) => [Number(value).toLocaleString('en-US'), _(msg`New Users`)]}
|
||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||
/>
|
||||
|
||||
@ -43,7 +49,7 @@ export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartPr
|
||||
fill="hsl(var(--primary))"
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={60}
|
||||
label="New Users"
|
||||
label={_(msg`New Users`)}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
@ -11,6 +13,8 @@ export type MonthlyTotalUsersChartProps = {
|
||||
};
|
||||
|
||||
export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersChartProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const formattedData = [...data].reverse().map(({ month, cume_count: count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
|
||||
@ -22,7 +26,9 @@ export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersCha
|
||||
<div className={className}>
|
||||
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">Total Users</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>Total Users</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@ -34,7 +40,7 @@ export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersCha
|
||||
labelStyle={{
|
||||
color: 'hsl(var(--primary-foreground))',
|
||||
}}
|
||||
formatter={(value) => [Number(value).toLocaleString('en-US'), 'Total Users']}
|
||||
formatter={(value) => [Number(value).toLocaleString('en-US'), _(msg`Total Users`)]}
|
||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||
/>
|
||||
|
||||
@ -43,7 +49,7 @@ export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersCha
|
||||
fill="hsl(var(--primary))"
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={60}
|
||||
label="Total Users"
|
||||
label={_(msg`Total Users`)}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getCompletedDocumentsMonthly } from '@documenso/lib/server-only/user/get-monthly-completed-document';
|
||||
import { getUserMonthlyGrowth } from '@documenso/lib/server-only/user/get-user-monthly-growth';
|
||||
|
||||
@ -128,6 +131,10 @@ const fetchEarlyAdopters = async () => {
|
||||
};
|
||||
|
||||
export default async function OpenPage() {
|
||||
setupI18nSSR();
|
||||
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [
|
||||
{ forks_count: forksCount, stargazers_count: stargazersCount },
|
||||
{ total_count: openIssues },
|
||||
@ -150,19 +157,23 @@ export default async function OpenPage() {
|
||||
<div>
|
||||
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">
|
||||
<Trans>Open Startup</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[60ch] text-center text-lg leading-normal">
|
||||
All our metrics, finances, and learnings are public. We believe in transparency and want
|
||||
to share our journey with you. You can read more about why here:{' '}
|
||||
<a
|
||||
className="font-bold"
|
||||
href="https://documenso.com/blog/pre-seed"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Announcing Open Metrics
|
||||
</a>
|
||||
<Trans>
|
||||
All our metrics, finances, and learnings are public. We believe in transparency and
|
||||
want to share our journey with you. You can read more about why here:{' '}
|
||||
<a
|
||||
className="font-bold"
|
||||
href="https://documenso.com/blog/pre-seed"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Announcing Open Metrics
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -180,12 +191,12 @@ export default async function OpenPage() {
|
||||
/>
|
||||
<MetricCard
|
||||
className="col-span-2 lg:col-span-1"
|
||||
title="Open Issues"
|
||||
title={_(msg`Open Issues`)}
|
||||
value={openIssues.toLocaleString('en-US')}
|
||||
/>
|
||||
<MetricCard
|
||||
className="col-span-2 lg:col-span-1"
|
||||
title="Merged PR's"
|
||||
title={_(msg`Merged PR's`)}
|
||||
value={mergedPullRequests.toLocaleString('en-US')}
|
||||
/>
|
||||
</div>
|
||||
@ -195,28 +206,32 @@ export default async function OpenPage() {
|
||||
<SalaryBands className="col-span-12" />
|
||||
</div>
|
||||
|
||||
<h2 className="px-4 text-2xl font-semibold">Finances</h2>
|
||||
<h2 className="px-4 text-2xl font-semibold">
|
||||
<Trans>Finances</Trans>
|
||||
</h2>
|
||||
<div className="mb-12 mt-4 grid grid-cols-12 gap-8">
|
||||
<FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
|
||||
|
||||
<CapTable className="col-span-12 lg:col-span-6" />
|
||||
</div>
|
||||
|
||||
<h2 className="px-4 text-2xl font-semibold">Community</h2>
|
||||
<h2 className="px-4 text-2xl font-semibold">
|
||||
<Trans>Community</Trans>
|
||||
</h2>
|
||||
<div className="mb-12 mt-4 grid grid-cols-12 gap-8">
|
||||
<BarMetric<StargazersType>
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="stars"
|
||||
title="GitHub: Total Stars"
|
||||
label="Stars"
|
||||
title={_(msg`GitHub: Total Stars`)}
|
||||
label={_(msg`Stars`)}
|
||||
className="col-span-12 lg:col-span-6"
|
||||
/>
|
||||
|
||||
<BarMetric<StargazersType>
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="mergedPRs"
|
||||
title="GitHub: Total Merged PRs"
|
||||
label="Merged PRs"
|
||||
title={_(msg`GitHub: Total Merged PRs`)}
|
||||
label={_(msg`Merged PRs`)}
|
||||
chartHeight={400}
|
||||
className="col-span-12 lg:col-span-6"
|
||||
/>
|
||||
@ -233,8 +248,8 @@ export default async function OpenPage() {
|
||||
<BarMetric<StargazersType>
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="openIssues"
|
||||
title="GitHub: Total Open Issues"
|
||||
label="Open Issues"
|
||||
title={_(msg`GitHub: Total Open Issues`)}
|
||||
label={_(msg`Open Issues`)}
|
||||
chartHeight={400}
|
||||
className="col-span-12 lg:col-span-6"
|
||||
/>
|
||||
@ -242,13 +257,15 @@ export default async function OpenPage() {
|
||||
<Typefully className="col-span-12 lg:col-span-6" />
|
||||
</div>
|
||||
|
||||
<h2 className="px-4 text-2xl font-semibold">Growth</h2>
|
||||
<h2 className="px-4 text-2xl font-semibold">
|
||||
<Trans>Growth</Trans>
|
||||
</h2>
|
||||
<div className="mb-12 mt-4 grid grid-cols-12 gap-8">
|
||||
<BarMetric<EarlyAdoptersType>
|
||||
data={EARLY_ADOPTERS_DATA}
|
||||
metricKey="earlyAdopters"
|
||||
title="Total Customers"
|
||||
label="Total Customers"
|
||||
title={_(msg`Total Customers`)}
|
||||
label={_(msg`Total Customers`)}
|
||||
className="col-span-12 lg:col-span-6"
|
||||
extraInfo={<OpenPageTooltip />}
|
||||
/>
|
||||
@ -268,11 +285,15 @@ export default async function OpenPage() {
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
|
||||
<h2 className="text-2xl font-bold">Is there more?</h2>
|
||||
<h2 className="text-2xl font-bold">
|
||||
<Trans>Is there more?</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center text-lg leading-normal">
|
||||
This page is evolving as we learn what makes a great signing company. We'll update it when
|
||||
we have more to share.
|
||||
<Trans>
|
||||
This page is evolving as we learn what makes a great signing company. We'll update it
|
||||
when we have more to share.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import {
|
||||
Table,
|
||||
@ -17,15 +19,23 @@ export type SalaryBandsProps = HTMLAttributes<HTMLDivElement>;
|
||||
export const SalaryBands = ({ className, ...props }: SalaryBandsProps) => {
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)} {...props}>
|
||||
<h3 className="px-4 text-lg font-semibold">Global Salary Bands</h3>
|
||||
<h3 className="px-4 text-lg font-semibold">
|
||||
<Trans>Global Salary Bands</Trans>
|
||||
</h3>
|
||||
|
||||
<div className="border-border mt-2.5 flex-1 rounded-2xl border shadow-sm hover:shadow">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">Title</TableHead>
|
||||
<TableHead>Seniority</TableHead>
|
||||
<TableHead className="w-[100px] text-right">Salary</TableHead>
|
||||
<TableHead className="w-[200px]">
|
||||
<Trans>Title</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Seniority</Trans>
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px] text-right">
|
||||
<Trans>Salary</Trans>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import {
|
||||
Table,
|
||||
@ -15,20 +18,36 @@ import { TEAM_MEMBERS } from './data';
|
||||
export type TeamMembersProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const TeamMembers = ({ className, ...props }: TeamMembersProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)} {...props}>
|
||||
<h2 className="px-4 text-2xl font-semibold">Team</h2>
|
||||
<h2 className="px-4 text-2xl font-semibold">
|
||||
<Trans>Team</Trans>
|
||||
</h2>
|
||||
|
||||
<div className="border-border mt-2.5 flex-1 rounded-2xl border shadow-sm hover:shadow">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="">Name</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Salary</TableHead>
|
||||
<TableHead>Engagement</TableHead>
|
||||
<TableHead>Location</TableHead>
|
||||
<TableHead className="w-[100px] text-right">Join Date</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Name</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Role</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Salary</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Engagement</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans>Location</Trans>
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px] text-right">
|
||||
<Trans>Join Date</Trans>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@ -44,7 +63,7 @@ export const TeamMembers = ({ className, ...props }: TeamMembersProps) => {
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell>{member.engagement}</TableCell>
|
||||
<TableCell>{_(member.engagement)}</TableCell>
|
||||
<TableCell>{member.location}</TableCell>
|
||||
<TableCell className="text-right">{member.joinDate}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@ -29,7 +31,9 @@ export function OpenPageTooltip() {
|
||||
</svg>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Customers with an Active Subscriptions.</p>
|
||||
<p>
|
||||
<Trans>Customers with an Active Subscriptions.</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
@ -11,6 +13,8 @@ export type TotalSignedDocumentsChartProps = {
|
||||
};
|
||||
|
||||
export const TotalSignedDocumentsChart = ({ className, data }: TotalSignedDocumentsChartProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const formattedData = [...data].reverse().map(({ month, cume_count: count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
|
||||
@ -22,7 +26,9 @@ export const TotalSignedDocumentsChart = ({ className, data }: TotalSignedDocume
|
||||
<div className={className}>
|
||||
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">Total Completed Documents</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>Total Completed Documents</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@ -46,7 +52,7 @@ export const TotalSignedDocumentsChart = ({ className, data }: TotalSignedDocume
|
||||
fill="hsl(var(--primary))"
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={60}
|
||||
label="Total Completed Documents"
|
||||
label={_(msg`Total Completed Documents`)}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@ -4,6 +4,7 @@ import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -15,22 +16,26 @@ export const Typefully = ({ className, ...props }: TypefullyProps) => {
|
||||
<div className={className} {...props}>
|
||||
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
|
||||
<div className="mb-6 flex px-4">
|
||||
<h3 className="text-lg font-semibold">Twitter Stats</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>Twitter Stats</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="my-12 flex flex-col items-center gap-y-4 text-center">
|
||||
<FaXTwitter className="h-12 w-12" />
|
||||
<Link href="https://typefully.com/documenso/stats" target="_blank">
|
||||
<h1>Documenso on X</h1>
|
||||
<h1>
|
||||
<Trans>Documenso on X</Trans>
|
||||
</h1>
|
||||
</Link>
|
||||
<Button className="rounded-full" size="sm" asChild>
|
||||
<Link href="https://typefully.com/documenso/stats" target="_blank">
|
||||
View all stats
|
||||
<Trans>View all stats</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button className="rounded-full bg-white" size="sm" asChild>
|
||||
<Link href="https://twitter.com/documenso" target="_blank">
|
||||
Follow us on X
|
||||
<Trans>Follow us on X</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Caveat } from 'next/font/google';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import { Callout } from '~/components/(marketing)/callout';
|
||||
@ -25,6 +26,8 @@ const fontCaveat = Caveat({
|
||||
});
|
||||
|
||||
export default async function IndexPage() {
|
||||
setupI18nSSR();
|
||||
|
||||
const starCount = await fetch('https://api.github.com/repos/documenso/documenso', {
|
||||
headers: {
|
||||
accept: 'application/vnd.github.v3+json',
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@ -28,15 +31,21 @@ export type PricingPageProps = {
|
||||
};
|
||||
|
||||
export default function PricingPage() {
|
||||
setupI18nSSR();
|
||||
|
||||
return (
|
||||
<div className="mt-6 sm:mt-12">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1>
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">
|
||||
<Trans>Pricing</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-foreground mt-4 text-lg leading-normal">
|
||||
Designed for every stage of your journey.
|
||||
<Trans>Designed for every stage of your journey.</Trans>
|
||||
</p>
|
||||
<p className="text-foreground text-lg leading-normal">
|
||||
<Trans>Get started today.</Trans>
|
||||
</p>
|
||||
<p className="text-foreground text-lg leading-normal">Get started today.</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
@ -49,19 +58,21 @@ export default function PricingPage() {
|
||||
|
||||
<div className="mx-auto mt-36 max-w-2xl">
|
||||
<h2 className="text-center text-2xl font-semibold">
|
||||
None of these work for you? Try self-hosting!
|
||||
<Trans>None of these work for you? Try self-hosting!</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-center leading-relaxed">
|
||||
Our self-hosted option is great for small teams and individuals who need a simple
|
||||
solution. You can use our docker based setup to get started in minutes. Take control with
|
||||
full customizability and data ownership.
|
||||
<Trans>
|
||||
Our self-hosted option is great for small teams and individuals who need a simple
|
||||
solution. You can use our docker based setup to get started in minutes. Take control
|
||||
with full customizability and data ownership.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Button variant="outline" size="lg" className="rounded-full hover:cursor-pointer" asChild>
|
||||
<Link href="https://github.com/documenso/documenso" target="_blank" rel="noreferrer">
|
||||
Get Started
|
||||
<Trans>Get Started</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@ -75,120 +86,134 @@ export default function PricingPage() {
|
||||
<Accordion type="multiple" className="mt-8">
|
||||
<AccordionItem value="plan-differences">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
What is the difference between the plans?
|
||||
<Trans>What is the difference between the plans?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
You can self-host Documenso for free or use our ready-to-use hosted version. The
|
||||
hosted version comes with additional support, painless scalability and more. Early
|
||||
adopters will get access to all features we build this year, for no additional cost!
|
||||
Forever! Yes, that includes multiple users per account later. If you want Documenso
|
||||
for your enterprise, we are happy to talk about your needs.
|
||||
<Trans>
|
||||
You can self-host Documenso for free or use our ready-to-use hosted version. The
|
||||
hosted version comes with additional support, painless scalability and more. Early
|
||||
adopters will get access to all features we build this year, for no additional cost!
|
||||
Forever! Yes, that includes multiple users per account later. If you want Documenso
|
||||
for your enterprise, we are happy to talk about your needs.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="data-handling">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
How do you handle my data?
|
||||
<Trans>How do you handle my data?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
Securely. Our data centers are located in Frankfurt (Germany), giving us the best
|
||||
local privacy laws. We are very aware of the sensitive nature of our data and follow
|
||||
best practices to ensure the security and integrity of the data entrusted to us.
|
||||
<Trans>
|
||||
Securely. Our data centers are located in Frankfurt (Germany), giving us the best
|
||||
local privacy laws. We are very aware of the sensitive nature of our data and follow
|
||||
best practices to ensure the security and integrity of the data entrusted to us.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="should-use-cloud">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
Why should I use your hosting service?
|
||||
<Trans>Why should I use your hosting service?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
Using our hosted version is the easiest way to get started, you can simply subscribe
|
||||
and start signing your documents. We take care of the infrastructure, so you can focus
|
||||
on your business. Additionally, when using our hosted version you benefit from our
|
||||
trusted signing certificates which helps you to build trust with your customers.
|
||||
<Trans>
|
||||
Using our hosted version is the easiest way to get started, you can simply subscribe
|
||||
and start signing your documents. We take care of the infrastructure, so you can
|
||||
focus on your business. Additionally, when using our hosted version you benefit from
|
||||
our trusted signing certificates which helps you to build trust with your customers.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="how-to-contribute">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
How can I contribute?
|
||||
<Trans>How can I contribute?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
That's awesome. You can take a look at the current{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://github.com/documenso/documenso/milestones"
|
||||
target="_blank"
|
||||
>
|
||||
Issues
|
||||
</Link>{' '}
|
||||
and join our{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
>
|
||||
Discord Community
|
||||
</Link>{' '}
|
||||
to keep up to date, on what the current priorities are. In any case, we are an open
|
||||
community and welcome all input, technical and non-technical ❤️
|
||||
<Trans>
|
||||
That's awesome. You can take a look at the current{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://github.com/documenso/documenso/milestones"
|
||||
target="_blank"
|
||||
>
|
||||
Issues
|
||||
</Link>{' '}
|
||||
and join our{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
>
|
||||
Discord Community
|
||||
</Link>{' '}
|
||||
to keep up to date, on what the current priorities are. In any case, we are an open
|
||||
community and welcome all input, technical and non-technical ❤️
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="can-i-use-documenso-commercially">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
Can I use Documenso commercially?
|
||||
<Trans>Can I use Documenso commercially?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
Yes! Documenso is offered under the GNU AGPL V3 open source license. This means you
|
||||
can use it for free and even modify it to fit your needs, as long as you publish your
|
||||
changes under the same license.
|
||||
<Trans>
|
||||
Yes! Documenso is offered under the GNU AGPL V3 open source license. This means you
|
||||
can use it for free and even modify it to fit your needs, as long as you publish
|
||||
your changes under the same license.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="why-prefer-documenso">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
Why should I prefer Documenso over DocuSign or some other signing tool?
|
||||
<Trans>Why should I prefer Documenso over DocuSign or some other signing tool?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
Documenso is a community effort to create an open and vibrant ecosystem around a tool,
|
||||
everybody is free to use and adapt. By being truly open we want to create trusted
|
||||
infrastructure for the future of the internet.
|
||||
<Trans>
|
||||
Documenso is a community effort to create an open and vibrant ecosystem around a
|
||||
tool, everybody is free to use and adapt. By being truly open we want to create
|
||||
trusted infrastructure for the future of the internet.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="where-can-i-get-support">
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
Where can I get support?
|
||||
<Trans>Where can I get support?</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
We are happy to assist you at{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="mailto:support@documenso.com"
|
||||
>
|
||||
support@documenso.com
|
||||
</Link>{' '}
|
||||
or{' '}
|
||||
<a
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
in our Discord-Support-Channel
|
||||
</a>{' '}
|
||||
please message either Lucas or Timur to get added to the channel if you are not
|
||||
already a member.
|
||||
<Trans>
|
||||
We are happy to assist you at{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="mailto:support@documenso.com"
|
||||
>
|
||||
support@documenso.com
|
||||
</Link>{' '}
|
||||
or{' '}
|
||||
<a
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
in our Discord-Support-Channel
|
||||
</a>{' '}
|
||||
please message either Lucas or Timur to get added to the channel if you are not
|
||||
already a member.
|
||||
</Trans>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { Caveat, Inter } from 'next/font/google';
|
||||
import { cookies, headers } from 'next/headers';
|
||||
|
||||
import { AxiomWebVitals } from 'next-axiom';
|
||||
import { PublicEnvScript } from 'next-runtime-env';
|
||||
|
||||
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
|
||||
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
|
||||
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
|
||||
import { TrpcProvider } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -54,9 +59,29 @@ export function generateMetadata() {
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const flags = await getAllAnonymousFlags();
|
||||
|
||||
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined;
|
||||
|
||||
// Should be safe to remove when we upgrade NextJS.
|
||||
// https://github.com/vercel/next.js/pull/65008
|
||||
// Currently if the middleware sets the cookie, it's not accessible in the cookies
|
||||
// during the same render.
|
||||
// So we go the roundabout way of checking the header for the set-cookie value.
|
||||
if (!cookies().get('i18n')) {
|
||||
const setCookieValue = headers().get('set-cookie');
|
||||
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
|
||||
|
||||
if (i18nCookie) {
|
||||
const i18n = i18nCookie.split('=')[1];
|
||||
|
||||
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
|
||||
}
|
||||
}
|
||||
|
||||
const { lang, i18n } = setupI18nSSR(overrideLang);
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
lang={lang}
|
||||
className={cn(fontInter.variable, fontCaveat.variable)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
@ -65,6 +90,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="google" content="notranslate" />
|
||||
<PublicEnvScript />
|
||||
</head>
|
||||
|
||||
@ -78,7 +104,11 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<FeatureFlagProvider initialFlags={flags}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<PlausibleProvider>
|
||||
<TrpcProvider>{children}</TrpcProvider>
|
||||
<TrpcProvider>
|
||||
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}>
|
||||
{children}
|
||||
</I18nClientProvider>
|
||||
</TrpcProvider>
|
||||
</PlausibleProvider>
|
||||
</ThemeProvider>
|
||||
</FeatureFlagProvider>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@ -13,16 +15,20 @@ export const CallToAction = ({ className, utmSource = 'generic-cta' }: CallToAct
|
||||
return (
|
||||
<Card spotlight className={className}>
|
||||
<CardContent className="flex flex-col items-center justify-center p-12">
|
||||
<h2 className="text-center text-2xl font-bold">Join the Open Signing Movement</h2>
|
||||
<h2 className="text-center text-2xl font-bold">
|
||||
<Trans>Join the Open Signing Movement</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center leading-normal">
|
||||
Create your account and start using state-of-the-art document signing. Open and beautiful
|
||||
signing is within your grasp.
|
||||
<Trans>
|
||||
Create your account and start using state-of-the-art document signing. Open and
|
||||
beautiful signing is within your grasp.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="mt-8 rounded-full no-underline" size="lg" asChild>
|
||||
<Link href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=${utmSource}`} target="_blank">
|
||||
Get started
|
||||
<Trans>Get started</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { LuGithub } from 'react-icons/lu';
|
||||
|
||||
@ -15,23 +16,6 @@ export type CalloutProps = {
|
||||
export const Callout = ({ starCount }: CalloutProps) => {
|
||||
const event = usePlausible();
|
||||
|
||||
const onSignUpClick = () => {
|
||||
const el = document.getElementById('email');
|
||||
|
||||
if (el) {
|
||||
const { top } = el.getBoundingClientRect();
|
||||
|
||||
window.scrollTo({
|
||||
top: top - 120,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
el.focus();
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-4">
|
||||
<Link href="https://app.documenso.com/signup?utm_source=marketing-callout">
|
||||
@ -40,9 +24,9 @@ export const Callout = ({ starCount }: CalloutProps) => {
|
||||
variant="outline"
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
>
|
||||
Try our Free Plan
|
||||
<Trans>Try our Free Plan</Trans>
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
No Credit Card required
|
||||
<Trans>No Credit Card required</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { AutoplayType } from 'embla-carousel-autoplay';
|
||||
import Autoplay from 'embla-carousel-autoplay';
|
||||
import useEmblaCarousel from 'embla-carousel-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
@ -13,13 +16,13 @@ import { Slide } from './slide';
|
||||
|
||||
const SLIDES = [
|
||||
{
|
||||
label: 'Signing Process',
|
||||
label: msg`Signing Process`,
|
||||
type: 'video',
|
||||
srcLight: 'https://github.com/documenso/design/raw/main/marketing/signing.webm',
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/signing.webm',
|
||||
},
|
||||
{
|
||||
label: 'Teams',
|
||||
label: msg`Teams`,
|
||||
type: 'video',
|
||||
srcLight: 'https://github.com/documenso/design/raw/main/marketing/teams.webm',
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/teams.webm',
|
||||
@ -31,7 +34,7 @@ const SLIDES = [
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/zapier.webm',
|
||||
},
|
||||
{
|
||||
label: 'Direct Link',
|
||||
label: msg`Direct Link`,
|
||||
type: 'video',
|
||||
srcLight: 'https://github.com/documenso/design/raw/main/marketing/direct-links.webm',
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/direct-links.webm',
|
||||
@ -49,7 +52,7 @@ const SLIDES = [
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/api.webm',
|
||||
},
|
||||
{
|
||||
label: 'Profile',
|
||||
label: msg`Profile`,
|
||||
type: 'video',
|
||||
srcLight: 'https://github.com/documenso/design/raw/main/marketing/profile_teaser.webm',
|
||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/profile_teaser.webm',
|
||||
@ -57,6 +60,8 @@ const SLIDES = [
|
||||
];
|
||||
|
||||
export const Carousel = () => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const slides = SLIDES;
|
||||
const [_isPlaying, setIsPlaying] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@ -73,6 +78,7 @@ export const Carousel = () => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }, [
|
||||
Autoplay({ playOnInit: true, delay: autoplayDelay[selectedIndex] || 5000 }),
|
||||
]);
|
||||
|
||||
const [emblaThumbsRef, emblaThumbsApi] = useEmblaCarousel(
|
||||
{
|
||||
loop: true,
|
||||
@ -84,19 +90,28 @@ export const Carousel = () => {
|
||||
|
||||
const onThumbClick = useCallback(
|
||||
(index: number) => {
|
||||
if (!emblaApi || !emblaThumbsApi) return;
|
||||
if (!emblaApi || !emblaThumbsApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
emblaApi.scrollTo(index);
|
||||
},
|
||||
[emblaApi, emblaThumbsApi],
|
||||
);
|
||||
|
||||
const onSelect = useCallback(() => {
|
||||
if (!emblaApi || !emblaThumbsApi) return;
|
||||
if (!emblaApi || !emblaThumbsApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedIndex(emblaApi.selectedScrollSnap());
|
||||
emblaThumbsApi.scrollTo(emblaApi.selectedScrollSnap());
|
||||
|
||||
resetProgress();
|
||||
const autoplay = emblaApi.plugins()?.autoplay;
|
||||
|
||||
// moduleResolution: bundler breaks this type
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const autoplay = emblaApi.plugins()?.autoplay as unknown as AutoplayType | undefined;
|
||||
|
||||
if (autoplay) {
|
||||
autoplay.reset();
|
||||
@ -167,11 +182,18 @@ export const Carousel = () => {
|
||||
}, [emblaApi, onSelect, mounted, resolvedTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
const autoplay = emblaApi?.plugins()?.autoplay;
|
||||
if (!autoplay) return;
|
||||
// moduleResolution: bundler breaks this type
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const autoplay = emblaApi?.plugins()?.autoplay as unknown as AutoplayType | undefined;
|
||||
|
||||
if (!autoplay || !emblaApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPlaying(autoplay.isPlaying());
|
||||
emblaApi
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(emblaApi as unknown as any)
|
||||
.on('autoplay:play', () => setIsPlaying(true))
|
||||
.on('autoplay:stop', () => setIsPlaying(false))
|
||||
.on('reInit', () => setIsPlaying(autoplay.isPlaying()));
|
||||
@ -233,7 +255,7 @@ export const Carousel = () => {
|
||||
src={resolvedTheme === 'dark' ? slide.srcDark : slide.srcLight}
|
||||
type="video/webm"
|
||||
/>
|
||||
Your browser does not support the video tag.
|
||||
<Trans>Your browser does not support the video tag.</Trans>
|
||||
</video>
|
||||
)}
|
||||
</div>
|
||||
@ -257,7 +279,7 @@ export const Carousel = () => {
|
||||
onClick={() => onThumbClick(index)}
|
||||
selected={index === selectedIndex}
|
||||
index={index}
|
||||
label={slide.label}
|
||||
label={typeof slide.label === 'string' ? slide.label : _(slide.label)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -12,13 +13,15 @@ export const Enterprise = () => {
|
||||
return (
|
||||
<div className="mx-auto mt-36 max-w-2xl">
|
||||
<h2 className="text-center text-2xl font-semibold">
|
||||
Enterprise Compliance, License or Technical Needs?
|
||||
<Trans>Enterprise Compliance, License or Technical Needs?</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-center leading-relaxed">
|
||||
Our Enterprise License is great for large organizations looking to switch to Documenso for all
|
||||
their signing needs. It's availible for our cloud offering as well as self-hosted setups and
|
||||
offers a wide range of compliance and Adminstration Features.
|
||||
<Trans>
|
||||
Our Enterprise License is great for large organizations looking to switch to Documenso for
|
||||
all their signing needs. It's available for our cloud offering as well as self-hosted
|
||||
setups and offers a wide range of compliance and Adminstration Features.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
@ -28,7 +31,9 @@ export const Enterprise = () => {
|
||||
className="mt-6"
|
||||
onClick={() => event('enterprise-contact')}
|
||||
>
|
||||
<Button className="rounded-full text-base">Contact Us</Button>
|
||||
<Button className="rounded-full text-base">
|
||||
<Trans>Contact Us</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,8 @@ import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import cardBeautifulFigure from '@documenso/assets/images/card-beautiful-figure.png';
|
||||
import cardFastFigure from '@documenso/assets/images/card-fast-figure.png';
|
||||
@ -25,17 +27,23 @@ export const FasterSmarterBeautifulBento = ({
|
||||
/>
|
||||
</div>
|
||||
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
|
||||
A 10x better signing experience.
|
||||
<span className="block md:mt-0">Faster, smarter and more beautiful.</span>
|
||||
<Trans>A 10x better signing experience.</Trans>
|
||||
<span className="block md:mt-0">
|
||||
<Trans>Faster, smarter and more beautiful.</Trans>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
|
||||
<Card className="col-span-2" degrees={45} gradient>
|
||||
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
|
||||
<p className="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
|
||||
<strong className="block">Fast.</strong>
|
||||
When it comes to sending or receiving a contract, you can count on lightning-fast
|
||||
speeds.
|
||||
<strong className="block">
|
||||
<Trans>Fast.</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
When it comes to sending or receiving a contract, you can count on lightning-fast
|
||||
speeds.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
|
||||
@ -51,9 +59,13 @@ export const FasterSmarterBeautifulBento = ({
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Beautiful.</strong>
|
||||
Because signing should be celebrated. That’s why we care about the smallest detail in
|
||||
our product.
|
||||
<strong className="block">
|
||||
<Trans>Beautiful.</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Because signing should be celebrated. That’s why we care about the smallest detail
|
||||
in our product.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
@ -69,8 +81,12 @@ export const FasterSmarterBeautifulBento = ({
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Smart.</strong>
|
||||
Our custom templates come with smart rules that can help you save time and energy.
|
||||
<strong className="block">
|
||||
<Trans>Smart.</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Our custom templates come with smart rules that can help you save time and energy.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
|
||||
@ -5,6 +5,8 @@ import type { HTMLAttributes } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
import { LiaDiscord } from 'react-icons/lia';
|
||||
import { LuGithub } from 'react-icons/lu';
|
||||
@ -13,6 +15,8 @@ import LogoImage from '@documenso/assets/logo.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||
|
||||
import { I18nSwitcher } from '~/components/(marketing)/i18n-switcher';
|
||||
|
||||
// import { StatusWidgetContainer } from './status-widget-container';
|
||||
|
||||
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
||||
@ -24,22 +28,24 @@ const SOCIAL_LINKS = [
|
||||
];
|
||||
|
||||
const FOOTER_LINKS = [
|
||||
{ href: '/pricing', text: 'Pricing' },
|
||||
{ href: '/pricing', text: msg`Pricing` },
|
||||
{ href: '/singleplayer', text: 'Singleplayer' },
|
||||
{ href: 'https://docs.documenso.com', text: 'Documentation', target: '_blank' },
|
||||
{ href: 'mailto:support@documenso.com', text: 'Support', target: '_blank' },
|
||||
{ href: '/blog', text: 'Blog' },
|
||||
{ href: '/changelog', text: 'Changelog' },
|
||||
{ href: '/open', text: 'Open Startup' },
|
||||
{ href: '/design-system', text: 'Design' },
|
||||
{ href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' },
|
||||
{ href: 'https://status.documenso.com', text: 'Status', target: '_blank' },
|
||||
{ href: '/oss-friends', text: 'OSS Friends' },
|
||||
{ href: '/careers', text: 'Careers' },
|
||||
{ href: '/privacy', text: 'Privacy' },
|
||||
{ href: 'https://docs.documenso.com', text: msg`Documentation`, target: '_blank' },
|
||||
{ href: 'mailto:support@documenso.com', text: msg`Support`, target: '_blank' },
|
||||
{ href: '/blog', text: msg`Blog` },
|
||||
{ href: '/changelog', text: msg`Changelog` },
|
||||
{ href: '/open', text: msg`Open Startup` },
|
||||
{ href: '/design-system', text: msg`Design` },
|
||||
{ href: 'https://shop.documenso.com', text: msg`Shop`, target: '_blank' },
|
||||
{ href: 'https://status.documenso.com', text: msg`Status`, target: '_blank' },
|
||||
{ href: '/oss-friends', text: msg`OSS Friends` },
|
||||
{ href: '/careers', text: msg`Careers` },
|
||||
{ href: '/privacy', text: msg`Privacy` },
|
||||
];
|
||||
|
||||
export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
return (
|
||||
<div className={cn('border-t py-12', className)} {...props}>
|
||||
<div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8">
|
||||
@ -80,7 +86,7 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
target={link.target}
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 break-words text-sm"
|
||||
>
|
||||
{link.text}
|
||||
{typeof link.text === 'string' ? link.text : _(link.text)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@ -90,8 +96,12 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
© {new Date().getFullYear()} Documenso, Inc. All rights reserved.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap">
|
||||
<ThemeSwitcher />
|
||||
<div className="flex flex-row-reverse items-center sm:flex-row">
|
||||
<I18nSwitcher className="text-muted-foreground ml-2 rounded-full font-normal sm:mr-2" />
|
||||
|
||||
<div className="flex flex-wrap">
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,8 +6,9 @@ import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import LogoImage from '@documenso/assets/logo.png';
|
||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
@ -19,10 +20,6 @@ export type HeaderProps = HTMLAttributes<HTMLElement>;
|
||||
export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||
|
||||
const { getFlag } = useFeatureFlags();
|
||||
|
||||
const isSinglePlayerModeMarketingEnabled = getFlag('marketing_header_single_player_mode');
|
||||
|
||||
return (
|
||||
<header className={cn('flex items-center justify-between', className)} {...props}>
|
||||
<div className="flex items-center space-x-4">
|
||||
@ -35,15 +32,6 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
height={25}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{isSinglePlayerModeMarketingEnabled && (
|
||||
<Link
|
||||
href="/singleplayer"
|
||||
className="bg-primary dark:text-background rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
|
||||
>
|
||||
Try now!
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center gap-x-6 md:flex">
|
||||
@ -51,7 +39,7 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
href="/pricing"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Pricing
|
||||
<Trans>Pricing</Trans>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
@ -66,14 +54,14 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
href="/blog"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Blog
|
||||
<Trans>Blog</Trans>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/open"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Open Startup
|
||||
<Trans>Open Startup</Trans>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
@ -81,12 +69,12 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Sign in
|
||||
<Trans>Sign in</Trans>
|
||||
</Link>
|
||||
|
||||
<Button className="rounded-full" size="sm" asChild>
|
||||
<Link href="https://app.documenso.com/signup?utm_source=marketing-header" target="_blank">
|
||||
Sign up
|
||||
<Trans>Sign up</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import type { Variants } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
@ -96,8 +97,11 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
animate="animate"
|
||||
className="text-center text-4xl font-bold leading-tight tracking-tight md:text-[48px] lg:text-[64px]"
|
||||
>
|
||||
Document signing,
|
||||
<span className="block" /> finally open source.
|
||||
<Trans>
|
||||
Document signing,
|
||||
<span className="block" />
|
||||
finally open source.
|
||||
</Trans>
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
@ -112,39 +116,21 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
variant="outline"
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
>
|
||||
Try our Free Plan
|
||||
<Trans>Try our Free Plan</Trans>
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
No Credit Card required
|
||||
<Trans>No Credit Card required</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
|
||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
||||
<LuGithub className="mr-2 h-5 w-5" />
|
||||
Star on GitHub
|
||||
<Trans>Star on GitHub</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{match(heroMarketingCTA)
|
||||
.with('spm', () => (
|
||||
<motion.div
|
||||
variants={HeroTitleVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
className="border-primary bg-background hover:bg-muted mx-auto mt-8 w-60 rounded-xl border transition-colors duration-300"
|
||||
>
|
||||
<Link href="/singleplayer" className="block px-4 py-2 text-center">
|
||||
<h2 className="text-muted-foreground text-xs font-semibold">
|
||||
Introducing Single Player Mode
|
||||
</h2>
|
||||
|
||||
<h1 className="text-foreground mt-1.5 font-medium leading-5">
|
||||
Self sign for free!
|
||||
</h1>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))
|
||||
.with('productHunt', () => (
|
||||
<motion.div
|
||||
variants={HeroTitleVariants}
|
||||
|
||||
71
apps/marketing/src/components/(marketing)/i18n-switcher.tsx
Normal file
71
apps/marketing/src/components/(marketing)/i18n-switcher.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
import { LuLanguages } from 'react-icons/lu';
|
||||
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@documenso/ui/primitives/command';
|
||||
|
||||
type I18nSwitcherProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const I18nSwitcher = ({ className }: I18nSwitcherProps) => {
|
||||
const { i18n, _ } = useLingui();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [value, setValue] = useState(i18n.locale);
|
||||
|
||||
const setLanguage = async (lang: string) => {
|
||||
setValue(lang);
|
||||
setOpen(false);
|
||||
|
||||
await dynamicActivate(i18n, lang);
|
||||
await switchI18NLanguage(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button className={className} variant="ghost" onClick={() => setOpen(true)}>
|
||||
<LuLanguages className="mr-1.5 h-4 w-4" />
|
||||
{SUPPORTED_LANGUAGES[value]?.full || i18n.locale}
|
||||
</Button>
|
||||
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder={_(msg`Search languages...`)} />
|
||||
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
{Object.values(SUPPORTED_LANGUAGES).map((language) => (
|
||||
<CommandItem
|
||||
key={language.short}
|
||||
value={language.full}
|
||||
onSelect={async () => setLanguage(language.short)}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
value === language.short ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
{SUPPORTED_LANGUAGES[language.short].full}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -3,6 +3,8 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
import { LiaDiscord } from 'react-icons/lia';
|
||||
@ -19,11 +21,11 @@ export type MobileNavigationProps = {
|
||||
export const MENU_NAVIGATION_LINKS = [
|
||||
{
|
||||
href: '/pricing',
|
||||
text: 'Pricing',
|
||||
text: msg`Pricing`,
|
||||
},
|
||||
{
|
||||
href: 'https://documen.so/docs-nav',
|
||||
text: 'Documentation',
|
||||
text: msg`Documentation`,
|
||||
},
|
||||
{
|
||||
href: '/singleplayer',
|
||||
@ -31,36 +33,38 @@ export const MENU_NAVIGATION_LINKS = [
|
||||
},
|
||||
{
|
||||
href: '/blog',
|
||||
text: 'Blog',
|
||||
text: msg`Blog`,
|
||||
},
|
||||
{
|
||||
href: '/open',
|
||||
text: 'Open Startup',
|
||||
text: msg`Open Startup`,
|
||||
},
|
||||
{
|
||||
href: 'https://status.documenso.com',
|
||||
text: 'Status',
|
||||
text: msg`Status`,
|
||||
},
|
||||
{
|
||||
href: 'mailto:support@documenso.com',
|
||||
text: 'Support',
|
||||
text: msg`Support`,
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
href: '/privacy',
|
||||
text: 'Privacy',
|
||||
text: msg`Privacy`,
|
||||
},
|
||||
{
|
||||
href: 'https://app.documenso.com/signup?utm_source=marketing-header',
|
||||
text: 'Sign up',
|
||||
text: msg`Sign up`,
|
||||
},
|
||||
{
|
||||
href: 'https://app.documenso.com/signin?utm_source=marketing-header',
|
||||
text: 'Sign in',
|
||||
text: msg`Sign in`,
|
||||
},
|
||||
];
|
||||
|
||||
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const handleMenuItemClick = () => {
|
||||
@ -112,7 +116,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
||||
onClick={() => handleMenuItemClick()}
|
||||
target={target}
|
||||
>
|
||||
{text}
|
||||
{typeof text === 'string' ? text : _(text)}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
@ -2,6 +2,8 @@ import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import cardBuildFigure from '@documenso/assets/images/card-build-figure.png';
|
||||
import cardOpenFigure from '@documenso/assets/images/card-open-figure.png';
|
||||
@ -22,17 +24,23 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
|
||||
/>
|
||||
</div>
|
||||
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
|
||||
Truly your own.
|
||||
<span className="block md:mt-0">Customise and expand.</span>
|
||||
<Trans>Truly your own.</Trans>
|
||||
<span className="block md:mt-0">
|
||||
<Trans>Customise and expand.</Trans>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
|
||||
<Card className="col-span-2" degrees={45} gradient>
|
||||
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
|
||||
<p className="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
|
||||
<strong className="block">Open Source or Hosted.</strong>
|
||||
It’s up to you. Either clone our repository or rely on our easy to use hosting
|
||||
solution.
|
||||
<strong className="block">
|
||||
<Trans>Open Source or Hosted.</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
It’s up to you. Either clone our repository or rely on our easy to use hosting
|
||||
solution.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
|
||||
@ -48,8 +56,10 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Build on top.</strong>
|
||||
Make it your own through advanced customization and adjustability.
|
||||
<strong className="block">
|
||||
<Trans>Build on top.</Trans>
|
||||
</strong>
|
||||
<Trans>Make it your own through advanced customization and adjustability.</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
@ -65,9 +75,13 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Template Store (Soon).</strong>
|
||||
Choose a template from the community app store. Or submit your own template for others
|
||||
to use.
|
||||
<strong className="block">
|
||||
<Trans>Template Store (Soon).</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Choose a template from the community app store. Or submit your own template for
|
||||
others to use.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
|
||||
@ -5,6 +5,7 @@ import { useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
|
||||
@ -36,7 +37,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
)}
|
||||
onClick={() => setPeriod('MONTHLY')}
|
||||
>
|
||||
Monthly
|
||||
<Trans>Monthly</Trans>
|
||||
{period === 'MONTHLY' && (
|
||||
<motion.div
|
||||
layoutId={SELECTED_PLAN_BAR_LAYOUT_ID}
|
||||
@ -56,9 +57,9 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
)}
|
||||
onClick={() => setPeriod('YEARLY')}
|
||||
>
|
||||
Yearly
|
||||
<Trans>Yearly</Trans>
|
||||
<div className="bg-muted text-foreground block rounded-full px-2 py-0.5 text-xs">
|
||||
Save $60 or $120
|
||||
<Trans>Save $60 or $120</Trans>
|
||||
</div>
|
||||
{period === 'YEARLY' && (
|
||||
<motion.div
|
||||
@ -75,11 +76,13 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
data-plan="free"
|
||||
className="bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border px-8 py-12 shadow-lg"
|
||||
>
|
||||
<p className="text-foreground text-4xl font-medium">Free</p>
|
||||
<p className="text-foreground text-4xl font-medium">
|
||||
<Trans>Free</Trans>
|
||||
</p>
|
||||
<p className="text-primary mt-2.5 text-xl font-medium">$0</p>
|
||||
|
||||
<p className="text-foreground mt-4 max-w-[30ch] text-center">
|
||||
For small teams and individuals with basic needs.
|
||||
<Trans>For small teams and individuals with basic needs.</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="rounded-full text-base" asChild>
|
||||
@ -88,14 +91,20 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
target="_blank"
|
||||
className="mt-6"
|
||||
>
|
||||
Signup Now
|
||||
<Trans>Signup Now</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="mt-8 flex w-full flex-col divide-y">
|
||||
<p className="text-foreground py-4">5 standard documents per month</p>
|
||||
<p className="text-foreground py-4">Up to 10 recipients per document</p>
|
||||
<p className="text-foreground py-4">No credit card required</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>5 standard documents per month</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Up to 10 recipients per document</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>No credit card required</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
@ -105,7 +114,9 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
data-plan="individual"
|
||||
className="bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border-2 px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380]"
|
||||
>
|
||||
<p className="text-foreground text-4xl font-medium">Individual</p>
|
||||
<p className="text-foreground text-4xl font-medium">
|
||||
<Trans>Individual</Trans>
|
||||
</p>
|
||||
<div className="text-primary mt-2.5 text-xl font-medium">
|
||||
<AnimatePresence mode="wait">
|
||||
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
|
||||
@ -114,7 +125,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</div>
|
||||
|
||||
<p className="text-foreground mt-4 max-w-[30ch] text-center">
|
||||
Everything you need for a great signing experience.
|
||||
<Trans>Everything you need for a great signing experience.</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="mt-6 rounded-full text-base" asChild>
|
||||
@ -122,15 +133,23 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=pricing-individual-plan`}
|
||||
target="_blank"
|
||||
>
|
||||
Signup Now
|
||||
<Trans>Signup Now</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="mt-8 flex w-full flex-col divide-y">
|
||||
<p className="text-foreground py-4">Unlimited Documents per Month</p>
|
||||
<p className="text-foreground py-4">API Access</p>
|
||||
<p className="text-foreground py-4">Email and Discord Support</p>
|
||||
<p className="text-foreground py-4">Premium Profile Name</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Unlimited Documents per Month</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>API Access</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Email and Discord Support</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Premium Profile Name</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
@ -139,7 +158,9 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
data-plan="teams"
|
||||
className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border px-8 py-12 shadow-lg"
|
||||
>
|
||||
<p className="text-foreground text-4xl font-medium">Teams</p>
|
||||
<p className="text-foreground text-4xl font-medium">
|
||||
<Trans>Teams</Trans>
|
||||
</p>
|
||||
<div className="text-primary mt-2.5 text-xl font-medium">
|
||||
<AnimatePresence mode="wait">
|
||||
{period === 'MONTHLY' && <motion.div layoutId="pricingTeams">$50</motion.div>}
|
||||
@ -148,7 +169,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</div>
|
||||
|
||||
<p className="text-foreground mt-4 max-w-[30ch] text-center">
|
||||
For companies looking to scale across multiple teams.
|
||||
<Trans>For companies looking to scale across multiple teams.</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="mt-6 rounded-full text-base" asChild>
|
||||
@ -156,18 +177,28 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=pricing-teams-plan`}
|
||||
target="_blank"
|
||||
>
|
||||
Signup Now
|
||||
<Trans>Signup Now</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="mt-8 flex w-full flex-col divide-y">
|
||||
<p className="text-foreground py-4">Unlimited Documents per Month</p>
|
||||
<p className="text-foreground py-4">API Access</p>
|
||||
<p className="text-foreground py-4">Email and Discord Support</p>
|
||||
<p className="text-foreground py-4 font-medium">Team Inbox</p>
|
||||
<p className="text-foreground py-4">5 Users Included</p>
|
||||
<p className="text-foreground py-4">
|
||||
Add More Users for {period === 'MONTHLY' ? '$10/ mo.' : '$96/ yr.'}
|
||||
<Trans>Unlimited Documents per Month</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>API Access</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Email and Discord Support</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4 font-medium">
|
||||
<Trans>Team Inbox</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>5 Users Included</Trans>
|
||||
</p>
|
||||
<p className="text-foreground py-4">
|
||||
<Trans>Add More Users for {period === 'MONTHLY' ? '$10/ mo.' : '$96/ yr.'}</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,8 @@ import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import cardConnectionsFigure from '@documenso/assets/images/card-connections-figure.png';
|
||||
import cardPaidFigure from '@documenso/assets/images/card-paid-figure.png';
|
||||
@ -26,16 +28,20 @@ export const ShareConnectPaidWidgetBento = ({
|
||||
/>
|
||||
</div>
|
||||
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
|
||||
Integrates with all your favourite tools.
|
||||
<span className="block md:mt-0">Send, connect, receive and embed everywhere.</span>
|
||||
<Trans>Integrates with all your favourite tools.</Trans>
|
||||
<span className="block md:mt-0">
|
||||
<Trans>Send, connect, receive and embed everywhere.</Trans>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
|
||||
<Card className="col-span-2 lg:col-span-1" degrees={120} gradient>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Easy Sharing (Soon).</strong>
|
||||
Receive your personal link to share with everyone you care about.
|
||||
<strong className="block">
|
||||
<Trans>Easy Sharing (Soon).</Trans>
|
||||
</strong>
|
||||
<Trans>Receive your personal link to share with everyone you care about.</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
@ -51,9 +57,13 @@ export const ShareConnectPaidWidgetBento = ({
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Connections</strong>
|
||||
Create connections and automations with Zapier and more to integrate with your
|
||||
favorite tools.
|
||||
<strong className="block">
|
||||
<Trans>Connections</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Create connections and automations with Zapier and more to integrate with your
|
||||
favorite tools.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
@ -69,8 +79,12 @@ export const ShareConnectPaidWidgetBento = ({
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">Get paid (Soon).</strong>
|
||||
Integrated payments with Stripe so you don’t have to worry about getting paid.
|
||||
<strong className="block">
|
||||
<Trans>Get paid (Soon).</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Integrated payments with Stripe so you don’t have to worry about getting paid.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
@ -86,9 +100,13 @@ export const ShareConnectPaidWidgetBento = ({
|
||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
||||
<p className="text-foreground/80 leading-relaxed">
|
||||
<strong className="block">React Widget (Soon).</strong>
|
||||
Easily embed Documenso into your product. Simply copy and paste our react widget into
|
||||
your application.
|
||||
<strong className="block">
|
||||
<Trans>React Widget (Soon).</Trans>
|
||||
</strong>
|
||||
<Trans>
|
||||
Easily embed Documenso into your product. Simply copy and paste our react widget
|
||||
into your application.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center p-8">
|
||||
|
||||
39
apps/marketing/src/middleware.ts
Normal file
39
apps/marketing/src/middleware.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
|
||||
|
||||
export default function middleware(req: NextRequest) {
|
||||
const lang = extractSupportedLanguage({
|
||||
headers: req.headers,
|
||||
cookies: cookies(),
|
||||
});
|
||||
|
||||
const response = NextResponse.next();
|
||||
|
||||
response.cookies.set('i18n', lang);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - ingest (analytics)
|
||||
* - site.webmanifest
|
||||
*/
|
||||
{
|
||||
source: '/((?!api|_next/static|_next/image|ingest|favicon|site.webmanifest).*)',
|
||||
missing: [
|
||||
{ type: 'header', key: 'next-router-prefetch' },
|
||||
{ type: 'header', key: 'purpose', value: 'prefetch' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user