diff --git a/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx b/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx
new file mode 100644
index 000000000..8526d4712
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx
@@ -0,0 +1,169 @@
+'use client';
+
+import { useEffect, useMemo, useState, useTransition } from 'react';
+
+import { msg } from '@lingui/macro';
+import { useLingui } from '@lingui/react';
+import { ChevronDownIcon as CaretSortIcon, Loader } from 'lucide-react';
+
+import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
+import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
+import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
+import { DataTable } from '@documenso/ui/primitives/data-table';
+import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
+import { Input } from '@documenso/ui/primitives/input';
+
+export type SigningVolume = {
+ id: number;
+ name: string;
+ signingVolume: number;
+ createdAt: Date;
+ planId: string;
+};
+
+type LeaderboardTableProps = {
+ signingVolume: SigningVolume[];
+ totalPages: number;
+ perPage: number;
+ page: number;
+ sortBy: 'name' | 'createdAt' | 'signingVolume';
+ sortOrder: 'asc' | 'desc';
+};
+
+export const LeaderboardTable = ({
+ signingVolume,
+ totalPages,
+ perPage,
+ page,
+ sortBy,
+ sortOrder,
+}: LeaderboardTableProps) => {
+ const { _, i18n } = useLingui();
+
+ const [isPending, startTransition] = useTransition();
+ const updateSearchParams = useUpdateSearchParams();
+ const [searchString, setSearchString] = useState('');
+ const debouncedSearchString = useDebouncedValue(searchString, 1000);
+
+ const columns = useMemo(() => {
+ return [
+ {
+ header: () => (
+
handleColumnSort('name')}
+ >
+ {_(msg`Name`)}
+
+
+ ),
+ accessorKey: 'name',
+ cell: ({ row }) => {
+ return (
+
+ );
+ },
+ size: 250,
+ },
+ {
+ header: () => (
+ handleColumnSort('signingVolume')}
+ >
+ {_(msg`Signing Volume`)}
+
+
+ ),
+ accessorKey: 'signingVolume',
+ cell: ({ row }) => {Number(row.getValue('signingVolume'))}
,
+ },
+ {
+ header: () => {
+ return (
+ handleColumnSort('createdAt')}
+ >
+ {_(msg`Created`)}
+
+
+ );
+ },
+ accessorKey: 'createdAt',
+ cell: ({ row }) => i18n.date(row.original.createdAt),
+ },
+ ] satisfies DataTableColumnDef[];
+ }, [sortOrder]);
+
+ useEffect(() => {
+ startTransition(() => {
+ updateSearchParams({
+ search: debouncedSearchString,
+ page: 1,
+ perPage,
+ sortBy,
+ sortOrder,
+ });
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [debouncedSearchString]);
+
+ const onPaginationChange = (page: number, perPage: number) => {
+ startTransition(() => {
+ updateSearchParams({
+ page,
+ perPage,
+ });
+ });
+ };
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setSearchString(e.target.value);
+ };
+
+ const handleColumnSort = (column: 'name' | 'createdAt' | 'signingVolume') => {
+ startTransition(() => {
+ updateSearchParams({
+ sortBy: column,
+ sortOrder: sortOrder === 'asc' ? 'desc' : 'asc',
+ });
+ });
+ };
+
+ return (
+
+
+
+ {(table) => }
+
+
+ {isPending && (
+
+
+
+ )}
+
+ );
+};
diff --git a/apps/web/src/app/(dashboard)/admin/leaderboard/fetch-leaderboard.actions.ts b/apps/web/src/app/(dashboard)/admin/leaderboard/fetch-leaderboard.actions.ts
new file mode 100644
index 000000000..42fc20c97
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/admin/leaderboard/fetch-leaderboard.actions.ts
@@ -0,0 +1,25 @@
+'use server';
+
+import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
+import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
+import { getSigningVolume } from '@documenso/lib/server-only/admin/get-signing-volume';
+
+type SearchOptions = {
+ search: string;
+ page: number;
+ perPage: number;
+ sortBy: 'name' | 'createdAt' | 'signingVolume';
+ sortOrder: 'asc' | 'desc';
+};
+
+export async function search({ search, page, perPage, sortBy, sortOrder }: SearchOptions) {
+ const { user } = await getRequiredServerComponentSession();
+
+ if (!isAdmin(user)) {
+ throw new Error('Unauthorized');
+ }
+
+ const results = await getSigningVolume({ search, page, perPage, sortBy, sortOrder });
+
+ return results;
+}
diff --git a/apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx b/apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx
new file mode 100644
index 000000000..0d1c5172a
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx
@@ -0,0 +1,60 @@
+import { Trans } from '@lingui/macro';
+
+import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
+import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
+import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
+
+import { LeaderboardTable } from './data-table-leaderboard';
+import { search } from './fetch-leaderboard.actions';
+
+type AdminLeaderboardProps = {
+ searchParams?: {
+ search?: string;
+ page?: number;
+ perPage?: number;
+ sortBy?: 'name' | 'createdAt' | 'signingVolume';
+ sortOrder?: 'asc' | 'desc';
+ };
+};
+
+export default async function Leaderboard({ searchParams = {} }: AdminLeaderboardProps) {
+ await setupI18nSSR();
+
+ const { user } = await getRequiredServerComponentSession();
+
+ if (!isAdmin(user)) {
+ throw new Error('Unauthorized');
+ }
+
+ const page = Number(searchParams.page) || 1;
+ const perPage = Number(searchParams.perPage) || 10;
+ const searchString = searchParams.search || '';
+ const sortBy = searchParams.sortBy || 'signingVolume';
+ const sortOrder = searchParams.sortOrder || 'desc';
+
+ const { leaderboard: signingVolume, totalPages } = await search({
+ search: searchString,
+ page,
+ perPage,
+ sortBy,
+ sortOrder,
+ });
+
+ return (
+
+
+ Signing Volume
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx
index cf0bb81f2..bcae0fc75 100644
--- a/apps/web/src/app/(dashboard)/admin/nav.tsx
+++ b/apps/web/src/app/(dashboard)/admin/nav.tsx
@@ -6,7 +6,7 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Trans } from '@lingui/macro';
-import { BarChart3, FileStack, Settings, Users, Wallet2 } from 'lucide-react';
+import { BarChart3, FileStack, Settings, Trophy, Users, Wallet2 } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -80,6 +80,20 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => {
+
+