diff --git a/apps/remix/app/components/general/period-selector.tsx b/apps/remix/app/components/general/period-selector.tsx
deleted file mode 100644
index 025925a92..000000000
--- a/apps/remix/app/components/general/period-selector.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { useMemo } from 'react';
-
-import { Trans } from '@lingui/react/macro';
-import { useLocation, useNavigate, useSearchParams } from 'react-router';
-
-import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-
-const isPeriodSelectorValue = (value: unknown): value is PeriodSelectorValue => {
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
- return ['', '7d', '14d', '30d'].includes(value as string);
-};
-
-export const PeriodSelector = () => {
- const { pathname } = useLocation();
- const [searchParams] = useSearchParams();
-
- const navigate = useNavigate();
-
- const period = useMemo(() => {
- const p = searchParams?.get('period') ?? 'all';
-
- return isPeriodSelectorValue(p) ? p : 'all';
- }, [searchParams]);
-
- const onPeriodChange = (newPeriod: string) => {
- if (!pathname) {
- return;
- }
-
- const params = new URLSearchParams(searchParams?.toString());
-
- params.set('period', newPeriod);
-
- if (newPeriod === '' || newPeriod === 'all') {
- params.delete('period');
- }
-
- void navigate(`${pathname}?${params.toString()}`, { preventScrollReset: true });
- };
-
- return (
-
- );
-};
diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts
index ba858a9c7..7446eb51a 100644
--- a/packages/lib/server-only/document/get-stats.ts
+++ b/packages/lib/server-only/document/get-stats.ts
@@ -1,17 +1,17 @@
import type { Prisma, User } from '@prisma/client';
import { DocumentVisibility, SigningStatus, TeamMemberRole } from '@prisma/client';
-import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
-import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
import { prisma } from '@documenso/prisma';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
+import type { TimePeriod } from '@documenso/ui/primitives/data-table/utils/time-filters';
+import { getDateRangeForPeriod } from '@documenso/ui/primitives/data-table/utils/time-filters';
export type GetStatsInput = {
user: User;
team?: Omit;
- period?: PeriodSelectorValue;
+ period?: TimePeriod;
search?: string;
folderId?: string;
};
@@ -26,60 +26,14 @@ export const getStats = async ({
let createdAt: Prisma.DocumentWhereInput['createdAt'];
if (period && period !== 'all-time') {
- const now = DateTime.now();
- let startDate: DateTime;
- let endDate: DateTime;
+ const dateRange = getDateRangeForPeriod(period);
- switch (period) {
- case 'today':
- startDate = now.startOf('day');
- endDate = now.endOf('day');
- break;
- case 'yesterday':
- startDate = now.minus({ days: 1 }).startOf('day');
- endDate = now.minus({ days: 1 }).endOf('day');
- break;
- case 'this-week':
- startDate = now.startOf('week');
- endDate = now.endOf('week');
- break;
- case 'last-week':
- startDate = now.minus({ weeks: 1 }).startOf('week');
- endDate = now.minus({ weeks: 1 }).endOf('week');
- break;
- case 'this-month':
- startDate = now.startOf('month');
- endDate = now.endOf('month');
- break;
- case 'last-month':
- startDate = now.minus({ months: 1 }).startOf('month');
- endDate = now.minus({ months: 1 }).endOf('month');
- break;
- case 'this-quarter':
- startDate = now.startOf('quarter');
- endDate = now.endOf('quarter');
- break;
- case 'last-quarter':
- startDate = now.minus({ quarters: 1 }).startOf('quarter');
- endDate = now.minus({ quarters: 1 }).endOf('quarter');
- break;
- case 'this-year':
- startDate = now.startOf('year');
- endDate = now.endOf('year');
- break;
- case 'last-year':
- startDate = now.minus({ years: 1 }).startOf('year');
- endDate = now.minus({ years: 1 }).endOf('year');
- break;
- default:
- startDate = now.startOf('day');
- endDate = now.endOf('day');
+ if (dateRange) {
+ createdAt = {
+ gte: dateRange.start.toJSDate(),
+ lte: dateRange.end.toJSDate(),
+ };
}
-
- createdAt = {
- gte: startDate.toJSDate(),
- lte: endDate.toJSDate(),
- };
}
const [ownerCounts, notSignedCounts, hasSignedCounts] = await (options.team
diff --git a/packages/ui/primitives/data-table.tsx b/packages/ui/primitives/data-table.tsx
new file mode 100644
index 000000000..d27dab0aa
--- /dev/null
+++ b/packages/ui/primitives/data-table.tsx
@@ -0,0 +1,167 @@
+import React, { useMemo } from 'react';
+
+import { Trans } from '@lingui/react/macro';
+import type {
+ ColumnDef,
+ PaginationState,
+ Table as TTable,
+ Updater,
+ VisibilityState,
+} from '@tanstack/react-table';
+import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
+
+import { Skeleton } from './skeleton';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './table';
+
+export type DataTableChildren = (_table: TTable) => React.ReactNode;
+
+export type { ColumnDef as DataTableColumnDef } from '@tanstack/react-table';
+
+export interface DataTableProps {
+ columns: ColumnDef[];
+ columnVisibility?: VisibilityState;
+ data: TData[];
+ perPage?: number;
+ currentPage?: number;
+ totalPages?: number;
+ onPaginationChange?: (_page: number, _perPage: number) => void;
+ onClearFilters?: () => void;
+ hasFilters?: boolean;
+ children?: DataTableChildren;
+ skeleton?: {
+ enable: boolean;
+ rows: number;
+ component?: React.ReactNode;
+ };
+ error?: {
+ enable: boolean;
+ component?: React.ReactNode;
+ };
+}
+
+export function DataTable({
+ columns,
+ columnVisibility,
+ data,
+ error,
+ perPage,
+ currentPage,
+ totalPages,
+ skeleton,
+ hasFilters,
+ onClearFilters,
+ onPaginationChange,
+ children,
+}: DataTableProps) {
+ const pagination = useMemo(() => {
+ if (currentPage !== undefined && perPage !== undefined) {
+ return {
+ pageIndex: currentPage - 1,
+ pageSize: perPage,
+ };
+ }
+
+ return {
+ pageIndex: 0,
+ pageSize: 0,
+ };
+ }, [currentPage, perPage]);
+
+ const manualPagination = Boolean(currentPage !== undefined && totalPages !== undefined);
+
+ const onTablePaginationChange = (updater: Updater) => {
+ if (typeof updater === 'function') {
+ const newState = updater(pagination);
+
+ onPaginationChange?.(newState.pageIndex + 1, newState.pageSize);
+ } else {
+ onPaginationChange?.(updater.pageIndex + 1, updater.pageSize);
+ }
+ };
+
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ state: {
+ pagination: manualPagination ? pagination : undefined,
+ columnVisibility,
+ },
+ manualPagination,
+ pageCount: totalPages,
+ onPaginationChange: onTablePaginationChange,
+ });
+
+ return (
+ <>
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => {
+ return (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(header.column.columnDef.header, header.getContext())}
+
+ );
+ })}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ ))
+ ) : error?.enable ? (
+
+ {error.component ?? (
+
+ Something went wrong.
+
+ )}
+
+ ) : skeleton?.enable ? (
+ Array.from({ length: skeleton.rows }).map((_, i) => (
+ {skeleton.component ?? }
+ ))
+ ) : (
+
+
+
+ No results found
+
+
+ {hasFilters && onClearFilters !== undefined && (
+
+ )}
+
+
+ )}
+
+
+
+
+ {children && {children(table)}
}
+ >
+ );
+}