mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: show monthly new users
This commit is contained in:
@ -19,7 +19,8 @@
|
|||||||
"@hookform/resolvers": "^3.1.0",
|
"@hookform/resolvers": "^3.1.0",
|
||||||
"contentlayer": "^0.3.4",
|
"contentlayer": "^0.3.4",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.277.0",
|
"lucide-react": "^0.279.0",
|
||||||
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "14.0.0",
|
"next": "14.0.0",
|
||||||
"next-auth": "4.24.3",
|
"next-auth": "4.24.3",
|
||||||
|
|||||||
@ -40,9 +40,9 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
|
|||||||
<span>{extraInfo}</span>
|
<span>{extraInfo}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow">
|
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border p-6 pl-2 pt-12 shadow-sm hover:shadow">
|
||||||
<ResponsiveContainer width="100%" height={chartHeight}>
|
<ResponsiveContainer width="100%" height={chartHeight}>
|
||||||
<BarChart data={formattedData} margin={{ top: 30, right: 20 }}>
|
<BarChart data={formattedData}>
|
||||||
<XAxis dataKey="month" />
|
<XAxis dataKey="month" />
|
||||||
<YAxis />
|
<YAxis />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -55,7 +55,13 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
|
|||||||
formatter={(value) => [Number(value), label]}
|
formatter={(value) => [Number(value), label]}
|
||||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey={metricKey as string} fill="hsl(var(--primary))" label={label} />{' '}
|
<Bar
|
||||||
|
dataKey={metricKey as string}
|
||||||
|
maxBarSize={60}
|
||||||
|
fill="hsl(var(--primary))"
|
||||||
|
label={label}
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
/>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps)
|
|||||||
<div className={cn('flex flex-col', className)} {...props}>
|
<div className={cn('flex flex-col', className)} {...props}>
|
||||||
<h3 className="px-4 text-lg font-semibold">Total Funding Raised</h3>
|
<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">
|
<div className="border-border mt-2.5 flex flex-1 flex-col items-center justify-center rounded-2xl border p-6 pl-2 pt-12 shadow-sm hover:shadow">
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
<BarChart data={formattedData} margin={{ top: 40, right: 40, bottom: 20, left: 40 }}>
|
<BarChart data={formattedData} margin={{ top: 40, right: 40, bottom: 20, left: 40 }}>
|
||||||
<XAxis dataKey="date" />
|
<XAxis dataKey="date" />
|
||||||
@ -51,7 +51,13 @@ export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps)
|
|||||||
]}
|
]}
|
||||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="amount" fill="hsl(var(--primary))" label="Amount Raised" />
|
<Bar
|
||||||
|
dataKey="amount"
|
||||||
|
fill="hsl(var(--primary))"
|
||||||
|
label="Amount Raised"
|
||||||
|
maxBarSize={60}
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
/>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
|
||||||
|
import { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
export type MonthlyUsersChartProps = {
|
||||||
|
className?: string;
|
||||||
|
data: GetUserMonthlyGrowthResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MonthlyUsersChart = ({ className, data }: MonthlyUsersChartProps) => {
|
||||||
|
const formattedData = [...data].reverse().map(({ month, count }) => {
|
||||||
|
return {
|
||||||
|
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'),
|
||||||
|
count: Number(count),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex flex-col', className)}>
|
||||||
|
<div className="flex items-center px-4">
|
||||||
|
<h3 className="text-lg font-semibold">Monthly New Users</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border p-6 pl-2 pt-12 shadow-sm hover:shadow">
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<BarChart data={formattedData}>
|
||||||
|
<XAxis dataKey="month" />
|
||||||
|
<YAxis />
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value) => [Number(value).toLocaleString('en-US'), 'Total Users']}
|
||||||
|
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Bar
|
||||||
|
dataKey="count"
|
||||||
|
fill="hsl(var(--primary))"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
maxBarSize={60}
|
||||||
|
label="New Users"
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { getUserMonthlyGrowth } from '@documenso/lib/server-only/user/get-user-monthly-growth';
|
||||||
|
|
||||||
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
|
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
|
||||||
import { MetricCard } from '~/app/(marketing)/open/metric-card';
|
import { MetricCard } from '~/app/(marketing)/open/metric-card';
|
||||||
import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
|
import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
|
||||||
@ -7,11 +9,14 @@ import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
|
|||||||
import { BarMetric } from './bar-metrics';
|
import { BarMetric } from './bar-metrics';
|
||||||
import { CapTable } from './cap-table';
|
import { CapTable } from './cap-table';
|
||||||
import { FundingRaised } from './funding-raised';
|
import { FundingRaised } from './funding-raised';
|
||||||
|
import { MonthlyUsersChart } from './monthly-users-chart';
|
||||||
import { TeamMembers } from './team-members';
|
import { TeamMembers } from './team-members';
|
||||||
import { OpenPageTooltip } from './tooltip';
|
import { OpenPageTooltip } from './tooltip';
|
||||||
|
|
||||||
export const revalidate = 3600;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
const ZGithubStatsResponse = z.object({
|
const ZGithubStatsResponse = z.object({
|
||||||
stargazers_count: z.number(),
|
stargazers_count: z.number(),
|
||||||
forks_count: z.number(),
|
forks_count: z.number(),
|
||||||
@ -43,14 +48,20 @@ export type StargazersType = z.infer<typeof ZStargazersLiveResponse>;
|
|||||||
export type EarlyAdoptersType = z.infer<typeof ZEarlyAdoptersResponse>;
|
export type EarlyAdoptersType = z.infer<typeof ZEarlyAdoptersResponse>;
|
||||||
|
|
||||||
export default async function OpenPage() {
|
export default async function OpenPage() {
|
||||||
|
const GITHUB_HEADERS: Record<string, string> = {
|
||||||
|
accept: 'application/vnd.github.v3+json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.NEXT_PRIVATE_GITHUB_TOKEN) {
|
||||||
|
GITHUB_HEADERS.authorization = `Bearer ${process.env.NEXT_PRIVATE_GITHUB_TOKEN}`;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
forks_count: forksCount,
|
forks_count: forksCount,
|
||||||
open_issues: openIssues,
|
open_issues: openIssues,
|
||||||
stargazers_count: stargazersCount,
|
stargazers_count: stargazersCount,
|
||||||
} = await fetch('https://api.github.com/repos/documenso/documenso', {
|
} = await fetch('https://api.github.com/repos/documenso/documenso', {
|
||||||
headers: {
|
headers: GITHUB_HEADERS,
|
||||||
accept: 'application/vnd.github.v3+json',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.then(async (res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.then((res) => ZGithubStatsResponse.parse(res));
|
.then((res) => ZGithubStatsResponse.parse(res));
|
||||||
@ -58,9 +69,7 @@ export default async function OpenPage() {
|
|||||||
const { total_count: mergedPullRequests } = await fetch(
|
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',
|
'https://api.github.com/search/issues?q=repo:documenso/documenso/+is:pr+merged:>=2010-01-01&page=0&per_page=1',
|
||||||
{
|
{
|
||||||
headers: {
|
headers: GITHUB_HEADERS,
|
||||||
accept: 'application/vnd.github.v3+json',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then(async (res) => res.json())
|
.then(async (res) => res.json())
|
||||||
@ -82,6 +91,8 @@ export default async function OpenPage() {
|
|||||||
.then(async (res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.then((res) => ZEarlyAdoptersResponse.parse(res));
|
.then((res) => ZEarlyAdoptersResponse.parse(res));
|
||||||
|
|
||||||
|
const MONTHLY_USERS = await getUserMonthlyGrowth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
@ -122,7 +133,7 @@ export default async function OpenPage() {
|
|||||||
|
|
||||||
<TeamMembers className="col-span-12" />
|
<TeamMembers className="col-span-12" />
|
||||||
|
|
||||||
<SalaryBands className="col-span-12 lg:col-span-6" />
|
<SalaryBands className="col-span-12" />
|
||||||
|
|
||||||
<FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
|
<FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
|
||||||
|
|
||||||
@ -172,6 +183,8 @@ export default async function OpenPage() {
|
|||||||
className="col-span-12 lg:col-span-6"
|
className="col-span-12 lg:col-span-6"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<MonthlyUsersChart data={MONTHLY_USERS} className="col-span-12 lg:col-span-6" />
|
||||||
|
|
||||||
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
|
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
|
||||||
<h2 className="text-2xl font-bold">Where's the rest?</h2>
|
<h2 className="text-2xl font-bold">Where's the rest?</h2>
|
||||||
|
|
||||||
|
|||||||
3
package-lock.json
generated
3
package-lock.json
generated
@ -42,7 +42,8 @@
|
|||||||
"@hookform/resolvers": "^3.1.0",
|
"@hookform/resolvers": "^3.1.0",
|
||||||
"contentlayer": "^0.3.4",
|
"contentlayer": "^0.3.4",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.277.0",
|
"lucide-react": "^0.279.0",
|
||||||
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "14.0.0",
|
"next": "14.0.0",
|
||||||
"next-auth": "4.24.3",
|
"next-auth": "4.24.3",
|
||||||
|
|||||||
32
packages/lib/server-only/user/get-user-monthly-growth.ts
Normal file
32
packages/lib/server-only/user/get-user-monthly-growth.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export type GetUserMonthlyGrowthResult = Array<{
|
||||||
|
month: string;
|
||||||
|
count: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type GetUserMonthlyGrowthQueryResult = Array<{
|
||||||
|
month: Date;
|
||||||
|
count: bigint;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const getUserMonthlyGrowth = async () => {
|
||||||
|
const result = await prisma.$queryRaw<GetUserMonthlyGrowthQueryResult>`
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('month', "createdAt") AS "month",
|
||||||
|
COUNT("id") AS "count"
|
||||||
|
FROM "User"
|
||||||
|
GROUP BY "month"
|
||||||
|
ORDER BY "month" DESC
|
||||||
|
LIMIT 12
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log('result', result);
|
||||||
|
|
||||||
|
return result.map((row) => ({
|
||||||
|
month: DateTime.fromJSDate(row.month).toFormat('yyyy-MM'),
|
||||||
|
count: Number(row.count),
|
||||||
|
}));
|
||||||
|
};
|
||||||
@ -82,6 +82,7 @@
|
|||||||
"NEXT_PRIVATE_SMTP_FROM_ADDRESS",
|
"NEXT_PRIVATE_SMTP_FROM_ADDRESS",
|
||||||
"NEXT_PRIVATE_STRIPE_API_KEY",
|
"NEXT_PRIVATE_STRIPE_API_KEY",
|
||||||
"NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET",
|
"NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET",
|
||||||
|
"NEXT_PRIVATE_GITHUB_TOKEN",
|
||||||
"VERCEL",
|
"VERCEL",
|
||||||
"VERCEL_ENV",
|
"VERCEL_ENV",
|
||||||
"VERCEL_URL",
|
"VERCEL_URL",
|
||||||
|
|||||||
Reference in New Issue
Block a user