diff --git a/README.md b/README.md index d6a5053f4..16738923c 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,22 @@ Contact us if you are interested in our Enterprise plan for large organizations Book us with Cal.com ## Tech Stack +

+ TypeScript + NextJS + Made with Prisma + Tailwind CSS + + + + + +

+ - [Typescript](https://www.typescriptlang.org/) - Language - [Next.js](https://nextjs.org/) - Framework -- [Prisma](https://www.prisma.io/) - ORM +- [Prisma](https://www.prisma.io/) - ORM - [Tailwind](https://tailwindcss.com/) - CSS - [shadcn/ui](https://ui.shadcn.com/) - Component Library - [NextAuth.js](https://next-auth.js.org/) - Authentication diff --git a/apps/marketing/src/components/(marketing)/carousel.tsx b/apps/marketing/src/components/(marketing)/carousel.tsx index 307d4a4f0..f3d903809 100644 --- a/apps/marketing/src/components/(marketing)/carousel.tsx +++ b/apps/marketing/src/components/(marketing)/carousel.tsx @@ -30,6 +30,12 @@ const SLIDES = [ srcLight: 'https://github.com/documenso/design/raw/main/marketing/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', type: 'video', diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index 43fe4be01..bcce0b608 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -14,18 +14,34 @@ import { import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats'; import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; import { + getUserWithAtLeastOneDocumentPerMonth, + getUserWithAtLeastOneDocumentSignedPerMonth, + getUserWithSignedDocumentMonthlyGrowth, getUsersCount, getUsersWithSubscriptionsCount, } from '@documenso/lib/server-only/admin/get-users-stats'; import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; +import { UserWithDocumentChart } from './user-with-document'; + 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(), getUsersWithSubscriptionsCount(), getDocumentStats(), getRecipientsStats(), + getUserWithAtLeastOneDocumentPerMonth(), + getUserWithAtLeastOneDocumentSignedPerMonth(), + getUserWithSignedDocumentMonthlyGrowth(), ]); return ( @@ -43,12 +59,11 @@ export default async function AdminStatsPage() { -
+

Document metrics

-
- +
@@ -58,7 +73,7 @@ export default async function AdminStatsPage() {

Recipients metrics

-
+
+ +
+

Charts

+
+ + +
+
); } diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx new file mode 100644 index 000000000..cf9f11e23 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx @@ -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 & { tooltip?: string }) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+

+ {`${tooltip} : `} + {payload[0].value} +

+
+ ); + } + + 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 ( +
+
+
+

{title}

+
+ + + + + + + } + labelStyle={{ + color: 'hsl(var(--primary-foreground))', + }} + cursor={{ fill: 'hsl(var(--primary) / 10%)' }} + /> + + + + +
+
+ ); +}; diff --git a/packages/lib/server-only/admin/get-users-stats.ts b/packages/lib/server-only/admin/get-users-stats.ts index 09892171a..0f4a2f0b4 100644 --- a/packages/lib/server-only/admin/get-users-stats.ts +++ b/packages/lib/server-only/admin/get-users-stats.ts @@ -1,5 +1,7 @@ +import { DateTime } from 'luxon'; + import { prisma } from '@documenso/prisma'; -import { SubscriptionStatus } from '@documenso/prisma/client'; +import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client'; export const getUsersCount = async () => { 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` + 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), + })); +}; diff --git a/packages/ui/styles/theme.css b/packages/ui/styles/theme.css index fa9231e5d..0b5bc587f 100644 --- a/packages/ui/styles/theme.css +++ b/packages/ui/styles/theme.css @@ -44,6 +44,8 @@ --radius: 0.5rem; --warning: 54 96% 45%; + + --gold: 47.9 95.8% 53.1%; } .dark { @@ -83,6 +85,8 @@ --radius: 0.5rem; --warning: 54 96% 45%; + + --gold: 47.9 95.8% 53.1%; } }