Merge branch 'main' into fix/show-sign-in-or-sign-up-for-account-required

This commit is contained in:
Lucas Smith
2024-06-26 11:48:15 +10:00
committed by GitHub
6 changed files with 220 additions and 7 deletions

View File

@ -73,6 +73,18 @@ Contact us if you are interested in our Enterprise plan for large organizations
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a> <a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
## Tech Stack ## Tech Stack
<p align="left">
<a href="https://www.typescriptlang.org"><img src="https://shields.io/badge/TypeScript-3178C6?logo=TypeScript&logoColor=FFF&style=flat-square" alt="TypeScript"></a>
<a href="https://nextjs.org/"><img src="https://img.shields.io/badge/next.js-000000?style=flat-square&logo=nextdotjs&logoColor=white" alt="NextJS"></a>
<a href="https://prisma.io"><img width="122" height="20" src="http://made-with.prisma.io/indigo.svg" alt="Made with Prisma" /></a>
<a href="https://tailwindcss.com/"><img src="https://img.shields.io/badge/tailwindcss-0F172A?&logo=tailwindcss" alt="Tailwind CSS"></a>
<a href=""><img src="" alt=""></a>
<a href=""><img src="" alt=""></a>
<a href=""><img src="" alt=""></a>
<a href=""><img src="" alt=""></a>
<a href=""><img src="" alt=""></a>
</p>
- [Typescript](https://www.typescriptlang.org/) - Language - [Typescript](https://www.typescriptlang.org/) - Language
- [Next.js](https://nextjs.org/) - Framework - [Next.js](https://nextjs.org/) - Framework

View File

@ -30,6 +30,12 @@ const SLIDES = [
srcLight: 'https://github.com/documenso/design/raw/main/marketing/zapier.webm', srcLight: 'https://github.com/documenso/design/raw/main/marketing/zapier.webm',
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/zapier.webm', srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/zapier.webm',
}, },
{
label: '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',
},
{ {
label: 'Webhooks', label: 'Webhooks',
type: 'video', type: 'video',

View File

@ -14,18 +14,34 @@ import {
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats'; import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
import { import {
getUserWithAtLeastOneDocumentPerMonth,
getUserWithAtLeastOneDocumentSignedPerMonth,
getUserWithSignedDocumentMonthlyGrowth,
getUsersCount, getUsersCount,
getUsersWithSubscriptionsCount, getUsersWithSubscriptionsCount,
} from '@documenso/lib/server-only/admin/get-users-stats'; } from '@documenso/lib/server-only/admin/get-users-stats';
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
import { UserWithDocumentChart } from './user-with-document';
export default async function AdminStatsPage() { export default async function AdminStatsPage() {
const [usersCount, usersWithSubscriptionsCount, docStats, recipientStats] = await Promise.all([ const [
usersCount,
usersWithSubscriptionsCount,
docStats,
recipientStats,
userWithAtLeastOneDocumentPerMonth,
userWithAtLeastOneDocumentSignedPerMonth,
MONTHLY_USERS_SIGNED,
] = await Promise.all([
getUsersCount(), getUsersCount(),
getUsersWithSubscriptionsCount(), getUsersWithSubscriptionsCount(),
getDocumentStats(), getDocumentStats(),
getRecipientsStats(), getRecipientsStats(),
getUserWithAtLeastOneDocumentPerMonth(),
getUserWithAtLeastOneDocumentSignedPerMonth(),
getUserWithSignedDocumentMonthlyGrowth(),
]); ]);
return ( return (
@ -43,12 +59,11 @@ export default async function AdminStatsPage() {
<CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} /> <CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} />
</div> </div>
<div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-1 lg:grid-cols-2"> <div className="mt-16 gap-8">
<div> <div>
<h3 className="text-3xl font-semibold">Document metrics</h3> <h3 className="text-3xl font-semibold">Document metrics</h3>
<div className="mt-8 grid flex-1 grid-cols-2 gap-4"> <div className="mb-8 mt-4 grid flex-1 grid-cols-1 gap-4 md:grid-cols-2">
<CardMetric icon={File} title="Total Documents" value={docStats.ALL} />
<CardMetric icon={FileEdit} title="Drafted Documents" value={docStats.DRAFT} /> <CardMetric icon={FileEdit} title="Drafted Documents" value={docStats.DRAFT} />
<CardMetric icon={FileClock} title="Pending Documents" value={docStats.PENDING} /> <CardMetric icon={FileClock} title="Pending Documents" value={docStats.PENDING} />
<CardMetric icon={FileCheck} title="Completed Documents" value={docStats.COMPLETED} /> <CardMetric icon={FileCheck} title="Completed Documents" value={docStats.COMPLETED} />
@ -58,7 +73,7 @@ export default async function AdminStatsPage() {
<div> <div>
<h3 className="text-3xl font-semibold">Recipients metrics</h3> <h3 className="text-3xl font-semibold">Recipients metrics</h3>
<div className="mt-8 grid flex-1 grid-cols-2 gap-4"> <div className="mb-8 mt-4 grid flex-1 grid-cols-1 gap-4 md:grid-cols-2">
<CardMetric <CardMetric
icon={UserSquare2} icon={UserSquare2}
title="Total Recipients" title="Total Recipients"
@ -70,6 +85,23 @@ export default async function AdminStatsPage() {
</div> </div>
</div> </div>
</div> </div>
<div className="mt-16">
<h3 className="text-3xl font-semibold">Charts</h3>
<div className="mt-5 grid grid-cols-2 gap-10">
<UserWithDocumentChart
data={MONTHLY_USERS_SIGNED}
title="MAU (created document)"
tooltip="Monthly Active Users: Users that created at least one Document"
/>
<UserWithDocumentChart
data={MONTHLY_USERS_SIGNED}
completed
title="MAU (had document completed)"
tooltip="Monthly Active Users: Users that had at least one of their documents completed"
/>
</div>
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,95 @@
'use client';
import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { TooltipProps } from 'recharts';
import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
export type UserWithDocumentChartProps = {
className?: string;
title: string;
data: GetUserWithDocumentMonthlyGrowth;
completed?: boolean;
tooltip?: string;
};
const CustomTooltip = ({
active,
payload,
label,
tooltip,
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
if (active && payload && payload.length) {
return (
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<p className="">{label}</p>
<p className="text-documenso">
{`${tooltip} : `}
<span className="text-black">{payload[0].value}</span>
</p>
</div>
);
}
return null;
};
export const UserWithDocumentChart = ({
className,
data,
title,
completed = false,
tooltip,
}: UserWithDocumentChartProps) => {
const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
return [...data].reverse().map(({ month, count, signed_count }) => {
const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL');
if (completed) {
return {
month: formattedMonth,
count: Number(signed_count),
};
} else {
return {
month: formattedMonth,
count: Number(count),
};
}
});
};
return (
<div className={className}>
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2">
<div className="mb-6 flex h-12 px-4">
<h3 className="text-lg font-semibold">{title}</h3>
</div>
<ResponsiveContainer width="100%" height={400}>
<BarChart className="bg-white" data={formattedData(data, completed)}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip
content={<CustomTooltip tooltip={tooltip} />}
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/>
<Bar
dataKey="count"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label={tooltip}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};

View File

@ -1,5 +1,7 @@
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client'; import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
export const getUsersCount = async () => { export const getUsersCount = async () => {
return await prisma.user.count(); return await prisma.user.count();
@ -16,3 +18,65 @@ export const getUsersWithSubscriptionsCount = async () => {
}, },
}); });
}; };
export const getUserWithAtLeastOneDocumentPerMonth = async () => {
return await prisma.user.count({
where: {
Document: {
some: {
createdAt: {
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
},
},
},
},
});
};
export const getUserWithAtLeastOneDocumentSignedPerMonth = async () => {
return await prisma.user.count({
where: {
Document: {
some: {
status: {
equals: DocumentStatus.COMPLETED,
},
completedAt: {
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
},
},
},
},
});
};
export type GetUserWithDocumentMonthlyGrowth = Array<{
month: string;
count: number;
signed_count: number;
}>;
type GetUserWithDocumentMonthlyGrowthQueryResult = Array<{
month: Date;
count: bigint;
signed_count: bigint;
}>;
export const getUserWithSignedDocumentMonthlyGrowth = async () => {
const result = await prisma.$queryRaw<GetUserWithDocumentMonthlyGrowthQueryResult>`
SELECT
DATE_TRUNC('month', "Document"."createdAt") AS "month",
COUNT(DISTINCT "Document"."userId") as "count",
COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END) as "signed_count"
FROM "Document"
GROUP BY "month"
ORDER BY "month" DESC
LIMIT 12
`;
return result.map((row) => ({
month: DateTime.fromJSDate(row.month).toFormat('yyyy-MM'),
count: Number(row.count),
signed_count: Number(row.signed_count),
}));
};

View File

@ -44,6 +44,8 @@
--radius: 0.5rem; --radius: 0.5rem;
--warning: 54 96% 45%; --warning: 54 96% 45%;
--gold: 47.9 95.8% 53.1%;
} }
.dark { .dark {
@ -83,6 +85,8 @@
--radius: 0.5rem; --radius: 0.5rem;
--warning: 54 96% 45%; --warning: 54 96% 45%;
--gold: 47.9 95.8% 53.1%;
} }
} }