mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Merge branch 'feat/refresh' into nsylke-patch-2
# Conflicts: # package-lock.json
This commit is contained in:
@ -28,6 +28,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-icons": "^4.8.0",
|
||||
"recharts": "^2.7.2",
|
||||
"typescript": "5.1.6",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
|
||||
82
apps/marketing/src/app/(marketing)/open/cap-table.tsx
Normal file
82
apps/marketing/src/app/(marketing)/open/cap-table.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes, useEffect, useState } from 'react';
|
||||
|
||||
import { Cell, Pie, PieChart, Tooltip } from 'recharts';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import { CAP_TABLE } from './data';
|
||||
|
||||
const COLORS = ['#7fd843', '#a2e771', '#c6f2a4'];
|
||||
const RADIAN = Math.PI / 180;
|
||||
|
||||
export type LabelRenderProps = {
|
||||
cx: number;
|
||||
cy: number;
|
||||
midAngle: number;
|
||||
innerRadius: number;
|
||||
outerRadius: number;
|
||||
percent: number;
|
||||
};
|
||||
|
||||
const renderCustomizedLabel = ({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
percent,
|
||||
}: LabelRenderProps) => {
|
||||
const radius = innerRadius + (outerRadius - innerRadius) * 0.25;
|
||||
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
|
||||
return (
|
||||
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
|
||||
{`${(percent * 100).toFixed(1)}%`}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
export type CapTableProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const CapTable = ({ className, ...props }: CapTableProps) => {
|
||||
const [isSSR, setIsSSR] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setIsSSR(false);
|
||||
}, []);
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)} {...props}>
|
||||
<h3 className="px-4 text-lg font-semibold">Cap Table</h3>
|
||||
|
||||
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border shadow-sm hover:shadow">
|
||||
{!isSSR && (
|
||||
<PieChart width={400} height={400}>
|
||||
<Pie
|
||||
data={CAP_TABLE}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={renderCustomizedLabel}
|
||||
outerRadius={180}
|
||||
innerRadius={100}
|
||||
fill="#8884d8"
|
||||
dataKey="percentage"
|
||||
>
|
||||
{CAP_TABLE.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={(percent: number, name, props) => {
|
||||
return [`${percent}%`, name || props['name'] || props['payload']['name']];
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
124
apps/marketing/src/app/(marketing)/open/data.ts
Normal file
124
apps/marketing/src/app/(marketing)/open/data.ts
Normal file
@ -0,0 +1,124 @@
|
||||
export const TEAM_MEMBERS = [
|
||||
{
|
||||
name: 'Timur Ercan',
|
||||
role: 'Co-Founder, CEO',
|
||||
salary: 95_000,
|
||||
location: 'Germany',
|
||||
engagement: 'Full-Time',
|
||||
joinDate: 'November 14th, 2022',
|
||||
},
|
||||
{
|
||||
name: 'Lucas Smith',
|
||||
role: 'Co-Founder, CTO',
|
||||
salary: 95_000,
|
||||
location: 'Australia',
|
||||
engagement: 'Full-Time',
|
||||
joinDate: 'April 19th, 2023',
|
||||
},
|
||||
{
|
||||
name: 'Ephraim Atta-Duncan',
|
||||
role: 'Software Engineer - Intern',
|
||||
salary: 15_000,
|
||||
location: 'Ghana',
|
||||
engagement: 'Part-Time',
|
||||
joinDate: 'June 6th, 2023',
|
||||
},
|
||||
{
|
||||
name: 'Florent Merian',
|
||||
role: 'Marketer - III',
|
||||
salary: 'Project-Based',
|
||||
location: 'France',
|
||||
engagement: 'Full-Time',
|
||||
joinDate: 'July 10th, 2023',
|
||||
},
|
||||
{
|
||||
name: 'Thilo Konzok',
|
||||
role: 'Designer',
|
||||
salary: 'Project-Based',
|
||||
location: 'Germany',
|
||||
engagement: 'Full-Time',
|
||||
joinDate: 'April 26th, 2023',
|
||||
},
|
||||
{
|
||||
name: 'David Nguyen',
|
||||
role: 'Software Engineer - III',
|
||||
salary: 100_000,
|
||||
location: 'Australia',
|
||||
engagement: 'Full-Time',
|
||||
joinDate: 'July 26th, 2023',
|
||||
},
|
||||
];
|
||||
|
||||
export const FUNDING_RAISED = [
|
||||
{
|
||||
date: '2023-04',
|
||||
amount: 0,
|
||||
},
|
||||
{
|
||||
date: '2023-05',
|
||||
amount: 300_000,
|
||||
},
|
||||
{
|
||||
date: '2023-07',
|
||||
amount: 1_550_000,
|
||||
},
|
||||
];
|
||||
|
||||
export const SALARY_BANDS = [
|
||||
{
|
||||
title: 'Software Engineer - Intern',
|
||||
seniority: 'Intern',
|
||||
salary: 30_000,
|
||||
},
|
||||
{
|
||||
title: 'Software Engineer - I',
|
||||
seniority: 'Junior',
|
||||
salary: 60_000,
|
||||
},
|
||||
{
|
||||
title: 'Software Engineer - II',
|
||||
seniority: 'Mid',
|
||||
salary: 80_000,
|
||||
},
|
||||
{
|
||||
title: 'Software Engineer - III',
|
||||
seniority: 'Senior',
|
||||
salary: 100_000,
|
||||
},
|
||||
{
|
||||
title: 'Software Engineer - IV',
|
||||
seniority: 'Principal',
|
||||
salary: 120_000,
|
||||
},
|
||||
{
|
||||
title: 'Designer - III',
|
||||
seniority: 'Senior',
|
||||
salary: 100_000,
|
||||
},
|
||||
{
|
||||
title: 'Designer - IV',
|
||||
seniority: 'Principal',
|
||||
salary: 120_000,
|
||||
},
|
||||
{
|
||||
title: 'Marketer - I',
|
||||
seniority: 'Junior',
|
||||
salary: 50_000,
|
||||
},
|
||||
{
|
||||
title: 'Marketer - II',
|
||||
seniority: 'Mid',
|
||||
salary: 65_000,
|
||||
},
|
||||
{
|
||||
title: 'Marketer - III',
|
||||
seniority: 'Senior',
|
||||
salary: 80_000,
|
||||
},
|
||||
];
|
||||
|
||||
export const CAP_TABLE = [
|
||||
{ name: 'Founders', percentage: 75.5 },
|
||||
{ name: 'Investors', percentage: 14.5 },
|
||||
{ name: 'Team Pool', percentage: 10 },
|
||||
];
|
||||
57
apps/marketing/src/app/(marketing)/open/funding-raised.tsx
Normal file
57
apps/marketing/src/app/(marketing)/open/funding-raised.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
|
||||
|
||||
export type FundingRaisedProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const FundingRaised = ({ className, ...props }: FundingRaisedProps) => {
|
||||
const formattedData = FUNDING_RAISED.map((item) => ({
|
||||
amount: Number(item.amount),
|
||||
date: formatMonth(item.date),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)} {...props}>
|
||||
<h3 className="px-4 text-lg font-semibold">Total Funding Raised</h3>
|
||||
|
||||
<div className="border-border mt-2.5 flex flex-1 flex-col items-center justify-center rounded-2xl border p-4 shadow-sm hover:shadow">
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<BarChart data={formattedData} margin={{ top: 40, right: 40, bottom: 20, left: 40 }}>
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis
|
||||
tickFormatter={(value) =>
|
||||
Number(value).toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Tooltip
|
||||
itemStyle={{
|
||||
color: 'hsl(var(--primary-foreground))',
|
||||
}}
|
||||
formatter={(value) => [
|
||||
Number(value).toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 0,
|
||||
}),
|
||||
'Amount Raised',
|
||||
]}
|
||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||
/>
|
||||
<Bar dataKey="amount" fill="hsl(var(--primary))" label="Amount Raised" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
59
apps/marketing/src/app/(marketing)/open/gh-metrics.tsx
Normal file
59
apps/marketing/src/app/(marketing)/open/gh-metrics.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import { StargazersType } from './page';
|
||||
|
||||
export type MetricsDataKey = 'stars' | 'forks' | 'mergedPRs' | 'openIssues';
|
||||
export type GithubMetricProps = HTMLAttributes<HTMLDivElement> & {
|
||||
data: StargazersType;
|
||||
metricKey: MetricsDataKey;
|
||||
title: string;
|
||||
label: string;
|
||||
chartHeight?: number;
|
||||
};
|
||||
|
||||
export const GithubMetric = ({
|
||||
className,
|
||||
data,
|
||||
metricKey,
|
||||
title,
|
||||
label,
|
||||
chartHeight = 400,
|
||||
...props
|
||||
}: GithubMetricProps) => {
|
||||
const formattedData = Object.keys(data)
|
||||
.map((key) => ({
|
||||
month: formatMonth(key),
|
||||
[metricKey]: data[key][metricKey],
|
||||
}))
|
||||
.reverse();
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)} {...props}>
|
||||
<h3 className="px-4 text-lg font-semibold">{title}</h3>
|
||||
|
||||
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow">
|
||||
<ResponsiveContainer width="100%" height={chartHeight}>
|
||||
<BarChart data={formattedData} margin={{ top: 30, right: 20 }}>
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis />
|
||||
<Tooltip
|
||||
itemStyle={{
|
||||
color: 'hsl(var(--primary-foreground))',
|
||||
}}
|
||||
formatter={(value) => [Number(value), label]}
|
||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||
/>
|
||||
<Bar dataKey={metricKey} fill="hsl(var(--primary))" label={label} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
18
apps/marketing/src/app/(marketing)/open/metric-card.tsx
Normal file
18
apps/marketing/src/app/(marketing)/open/metric-card.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
export type MetricCardProps = HTMLAttributes<HTMLDivElement> & {
|
||||
title: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const MetricCard = ({ className, title, value, ...props }: MetricCardProps) => {
|
||||
return (
|
||||
<div className={cn('rounded-md border p-4 shadow-sm hover:shadow', className)} {...props}>
|
||||
<h4 className="text-muted-foreground text-sm font-medium">{title}</h4>
|
||||
|
||||
<p className="mb-2 mt-6 text-4xl font-bold">{value}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
154
apps/marketing/src/app/(marketing)/open/page.tsx
Normal file
154
apps/marketing/src/app/(marketing)/open/page.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { MetricCard } from '~/app/(marketing)/open/metric-card';
|
||||
import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
|
||||
|
||||
import { CapTable } from './cap-table';
|
||||
import { FundingRaised } from './funding-raised';
|
||||
import { GithubMetric } from './gh-metrics';
|
||||
import { TeamMembers } from './team-members';
|
||||
|
||||
export const revalidate = 86400;
|
||||
|
||||
const ZGithubStatsResponse = z.object({
|
||||
stargazers_count: z.number(),
|
||||
forks_count: z.number(),
|
||||
open_issues: z.number(),
|
||||
});
|
||||
|
||||
const ZMergedPullRequestsResponse = z.object({
|
||||
total_count: z.number(),
|
||||
});
|
||||
|
||||
const ZStargazersLiveResponse = z.record(
|
||||
z.object({
|
||||
stars: z.number(),
|
||||
forks: z.number(),
|
||||
mergedPRs: z.number(),
|
||||
openIssues: z.number(),
|
||||
}),
|
||||
);
|
||||
|
||||
export type StargazersType = z.infer<typeof ZStargazersLiveResponse>;
|
||||
|
||||
// const ZOpenPullRequestsResponse = ZMergedPullRequestsResponse;
|
||||
|
||||
export default async function OpenPage() {
|
||||
const {
|
||||
forks_count: forksCount,
|
||||
open_issues: openIssues,
|
||||
stargazers_count: stargazersCount,
|
||||
} = await fetch('https://api.github.com/repos/documenso/documenso', {
|
||||
headers: {
|
||||
accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => ZGithubStatsResponse.parse(res));
|
||||
|
||||
const { total_count: mergedPullRequests } = await fetch(
|
||||
'https://api.github.com/search/issues?q=repo:documenso/documenso/+is:pr+merged:>=2010-01-01&page=0&per_page=1',
|
||||
{
|
||||
headers: {
|
||||
accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((res) => ZMergedPullRequestsResponse.parse(res));
|
||||
|
||||
const STARGAZERS_DATA = await fetch('https://stargrazer-live.onrender.com/api/stats', {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => ZStargazersLiveResponse.parse(res));
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-12 max-w-screen-lg">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[55ch] 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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 grid grid-cols-12 gap-8">
|
||||
<div className="col-span-12 grid grid-cols-4 gap-4">
|
||||
<MetricCard
|
||||
className="col-span-2 lg:col-span-1"
|
||||
title="Stargazers"
|
||||
value={stargazersCount.toLocaleString('en-US')}
|
||||
/>
|
||||
<MetricCard
|
||||
className="col-span-2 lg:col-span-1"
|
||||
title="Forks"
|
||||
value={forksCount.toLocaleString('en-US')}
|
||||
/>
|
||||
<MetricCard
|
||||
className="col-span-2 lg:col-span-1"
|
||||
title="Open Issues"
|
||||
value={openIssues.toLocaleString('en-US')}
|
||||
/>
|
||||
<MetricCard
|
||||
className="col-span-2 lg:col-span-1"
|
||||
title="Merged PR's"
|
||||
value={mergedPullRequests.toLocaleString('en-US')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TeamMembers className="col-span-12" />
|
||||
|
||||
<SalaryBands className="col-span-12 lg:col-span-6" />
|
||||
|
||||
<FundingRaised className="col-span-12 lg:col-span-6" />
|
||||
|
||||
<CapTable className="col-span-12 lg:col-span-6" />
|
||||
<GithubMetric
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="stars"
|
||||
title="Github: Total Stars"
|
||||
label="Stars"
|
||||
className="col-span-12 lg:col-span-6"
|
||||
/>
|
||||
|
||||
<GithubMetric
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="mergedPRs"
|
||||
title="Github: Total Merged PRs"
|
||||
label="Merged PRs"
|
||||
chartHeight={300}
|
||||
className="col-span-12 lg:col-span-4"
|
||||
/>
|
||||
<GithubMetric
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="forks"
|
||||
title="Github: Total Forks"
|
||||
label="Forks"
|
||||
chartHeight={300}
|
||||
className="col-span-12 lg:col-span-4"
|
||||
/>
|
||||
<GithubMetric
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="openIssues"
|
||||
title="Github: Total Open Issues"
|
||||
label="Open Issues"
|
||||
chartHeight={300}
|
||||
className="col-span-12 lg:col-span-4"
|
||||
/>
|
||||
|
||||
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
|
||||
<h2 className="text-2xl font-bold">Where's the rest?</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center text-lg leading-normal">
|
||||
We're still working on getting all our metrics together. We'll update this page as soon
|
||||
as we have more to share.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
apps/marketing/src/app/(marketing)/open/salary-bands.tsx
Normal file
50
apps/marketing/src/app/(marketing)/open/salary-bands.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@documenso/ui/primitives/table';
|
||||
|
||||
import { SALARY_BANDS } from '~/app/(marketing)/open/data';
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{SALARY_BANDS.map((band, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="font-medium">{band.title}</TableCell>
|
||||
<TableCell>{band.seniority}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{band.salary.toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
57
apps/marketing/src/app/(marketing)/open/team-members.tsx
Normal file
57
apps/marketing/src/app/(marketing)/open/team-members.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@documenso/ui/primitives/table';
|
||||
|
||||
import { TEAM_MEMBERS } from './data';
|
||||
|
||||
export type TeamMembersProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const TeamMembers = ({ className, ...props }: TeamMembersProps) => {
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)} {...props}>
|
||||
<h2 className="px-4 text-2xl font-semibold">Team</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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{TEAM_MEMBERS.map((member) => (
|
||||
<TableRow key={member.name}>
|
||||
<TableCell className="font-medium">{member.name}</TableCell>
|
||||
<TableCell>{member.role}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{member.salary.toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell>{member.engagement}</TableCell>
|
||||
<TableCell>{member.location}</TableCell>
|
||||
<TableCell className="text-right">{member.joinDate}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -9,6 +9,8 @@ import { Hero } from '~/components/(marketing)/hero';
|
||||
import { OpenBuildTemplateBento } from '~/components/(marketing)/open-build-template-bento';
|
||||
import { ShareConnectPaidWidgetBento } from '~/components/(marketing)/share-connect-paid-widget-bento';
|
||||
|
||||
export const revalidate = 600;
|
||||
|
||||
const fontCaveat = Caveat({
|
||||
weight: ['500'],
|
||||
subsets: ['latin'],
|
||||
@ -17,15 +19,24 @@ const fontCaveat = Caveat({
|
||||
});
|
||||
|
||||
export default async function IndexPage() {
|
||||
const starCount = await fetch('https://api.github.com/repos/documenso/documenso', {
|
||||
headers: {
|
||||
accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => (typeof res.stargazers_count === 'number' ? res.stargazers_count : undefined))
|
||||
.catch(() => undefined);
|
||||
|
||||
return (
|
||||
<div className={cn('mt-12', fontCaveat.variable)}>
|
||||
<Hero />
|
||||
<Hero starCount={starCount} />
|
||||
|
||||
<FasterSmarterBeautifulBento className="my-48" />
|
||||
<ShareConnectPaidWidgetBento className="my-48" />
|
||||
<OpenBuildTemplateBento className="my-48" />
|
||||
|
||||
<Callout />
|
||||
<Callout starCount={starCount} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,7 +7,12 @@ import { usePlausible } from 'next-plausible';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export const Callout = () => {
|
||||
export type CalloutProps = {
|
||||
starCount?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export const Callout = ({ starCount }: CalloutProps) => {
|
||||
const event = usePlausible();
|
||||
|
||||
const onSignUpClick = () => {
|
||||
@ -36,7 +41,7 @@ export const Callout = () => {
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Get the Community Plan
|
||||
<span className="bg-primary -mr-2 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
$30/mo. forever!
|
||||
</span>
|
||||
</Button>
|
||||
@ -49,6 +54,11 @@ export const Callout = () => {
|
||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
||||
<Github className="mr-2 h-5 w-5" />
|
||||
Star on Github
|
||||
{starCount && starCount > 0 && (
|
||||
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
{starCount.toLocaleString('en-US')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@ -7,7 +7,12 @@ import { usePlausible } from 'next-plausible';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export const Callout = () => {
|
||||
export type CalloutProps = {
|
||||
starCount?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export const Callout = ({ starCount }: CalloutProps) => {
|
||||
const event = usePlausible();
|
||||
|
||||
const onSignUpClick = () => {
|
||||
@ -36,21 +41,26 @@ export const Callout = () => {
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Get the Community Plan
|
||||
<span className="bg-primary -mr-2 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
$30/mo. forever!
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm" asChild>
|
||||
<Link
|
||||
href="https://github.com/documenso/documenso"
|
||||
target="_blank"
|
||||
onClick={() => event('view-github')}
|
||||
>
|
||||
<Link
|
||||
href="https://github.com/documenso/documenso"
|
||||
target="_blank"
|
||||
onClick={() => event('view-github')}
|
||||
>
|
||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
||||
<Github className="mr-2 h-5 w-5" />
|
||||
Star on Github
|
||||
</Link>
|
||||
</Button>
|
||||
{starCount && starCount > 0 && (
|
||||
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
{starCount.toLocaleString('en-US')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -16,6 +16,7 @@ import { Widget } from './widget';
|
||||
|
||||
export type HeroProps = {
|
||||
className?: string;
|
||||
starCount?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@ -48,7 +49,7 @@ const HeroTitleVariants: Variants = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
export const Hero = ({ className, starCount, ...props }: HeroProps) => {
|
||||
const event = usePlausible();
|
||||
|
||||
const onSignUpClick = () => {
|
||||
@ -109,44 +110,44 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Get the Community Plan
|
||||
<span className="bg-primary -mr-2 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
$30/mo. forever!
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href="https://github.com/documenso/documenso"
|
||||
onClick={() => event('view-github')}
|
||||
>
|
||||
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
|
||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
||||
<Github className="mr-2 h-5 w-5" />
|
||||
Star on Github
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={HeroTitleVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-4"
|
||||
>
|
||||
<Link
|
||||
href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily"
|
||||
alt="Documenso - The open source DocuSign alternative | Product Hunt"
|
||||
style={{ width: '250px', height: '54px' }}
|
||||
/>
|
||||
{starCount && starCount > 0 && (
|
||||
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
{starCount.toLocaleString('en-US')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-6">
|
||||
<motion.div
|
||||
variants={HeroTitleVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-4"
|
||||
>
|
||||
<Link
|
||||
href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily"
|
||||
alt="Documenso - The open source DocuSign alternative | Product Hunt"
|
||||
style={{ width: '250px', height: '54px' }}
|
||||
/>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="mt-12"
|
||||
variants={{
|
||||
|
||||
11580
package-lock.json
generated
11580
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,5 +23,8 @@
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
]
|
||||
],
|
||||
"dependencies": {
|
||||
"recharts": "^2.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
18
packages/lib/client-only/format-month.ts
Normal file
18
packages/lib/client-only/format-month.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export const formatMonth = (monthStr: string) => {
|
||||
const [year, month] = monthStr.split('-');
|
||||
const monthNames = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
return `${monthNames[parseInt(month, 10) - 1]} ${year}`;
|
||||
};
|
||||
Reference in New Issue
Block a user