diff --git a/apps/web/src/app/(dashboard)/dashboard/page.tsx b/apps/web/src/app/(dashboard)/dashboard/page.tsx
deleted file mode 100644
index 77b18b98c..000000000
--- a/apps/web/src/app/(dashboard)/dashboard/page.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import Link from 'next/link';
-
-import { Clock, File, FileCheck } from 'lucide-react';
-
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
-import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
-import { getStats } from '@documenso/lib/server-only/document/get-stats';
-import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@documenso/ui/primitives/table';
-
-import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
-import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
-import { DocumentStatus } from '~/components/formatter/document-status';
-import { LocaleDate } from '~/components/formatter/locale-date';
-
-import { UploadDocument } from './upload-document';
-
-const CARD_DATA = [
- {
- icon: FileCheck,
- title: 'Completed',
- status: InternalDocumentStatus.COMPLETED,
- },
- {
- icon: File,
- title: 'Drafts',
- status: InternalDocumentStatus.DRAFT,
- },
- {
- icon: Clock,
- title: 'Pending',
- status: InternalDocumentStatus.PENDING,
- },
-];
-
-export default async function DashboardPage() {
- const user = await getRequiredServerComponentSession();
-
- const [stats, results] = await Promise.all([
- getStats({
- user,
- }),
- findDocuments({
- userId: user.id,
- perPage: 10,
- }),
- ]);
-
- return (
-
-
Dashboard
-
-
- {CARD_DATA.map((card) => (
-
-
-
- ))}
-
-
-
-
-
-
Recent Documents
-
-
-
-
-
- ID
- Title
- Reciepient
- Status
- Created
-
-
-
- {results.data.map((document) => {
- return (
-
- {document.id}
-
-
- {document.title}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- })}
- {results.data.length === 0 && (
-
-
- No results.
-
-
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
index 7ed28feca..ba134ac58 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
+++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
@@ -136,7 +136,7 @@ export const EditDocumentForm = ({
duration: 5000,
});
- router.push('/dashboard');
+ router.push('/documents');
} catch (err) {
console.error(err);
diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx
index 245734a8e..b8c735b59 100644
--- a/apps/web/src/app/(dashboard)/documents/data-table.tsx
+++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx
@@ -52,8 +52,9 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
,
},
{
header: 'Title',
@@ -71,11 +72,6 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
accessorKey: 'status',
cell: ({ row }) => ,
},
- {
- header: 'Created',
- accessorKey: 'created',
- cell: ({ row }) => ,
- },
{
header: 'Actions',
cell: ({ row }) => (
@@ -92,7 +88,7 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
>
- {(table) => }
+ {(table) => }
{isPending && (
diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx
index 2738bfc4f..e437cdbdd 100644
--- a/apps/web/src/app/(dashboard)/documents/page.tsx
+++ b/apps/web/src/app/(dashboard)/documents/page.tsx
@@ -11,8 +11,8 @@ import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-
import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
import { DocumentStatus } from '~/components/formatter/document-status';
-import { UploadDocument } from '../dashboard/upload-document';
import { DocumentsDataTable } from './data-table';
+import { UploadDocument } from './upload-document';
export type DocumentsPageProps = {
searchParams?: {
@@ -71,35 +71,15 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
-
- {Math.min(stats.PENDING, 99)}
-
-
-
-
-
-
-
-
-
- {Math.min(stats.COMPLETED, 99)}
-
-
-
-
-
-
-
-
-
- {Math.min(stats.DRAFT, 99)}
-
-
-
-
-
- All
-
+ {value !== ExtendedDocumentStatus.ALL && (
+
+ {Math.min(stats[value], 99)}
+ {stats[value] > 99 && '+'}
+
+ )}
+
+
+ ))}
diff --git a/apps/web/src/app/(dashboard)/dashboard/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx
similarity index 100%
rename from apps/web/src/app/(dashboard)/dashboard/upload-document.tsx
rename to apps/web/src/app/(dashboard)/documents/upload-document.tsx
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 1d1e056ae..2ce8744d4 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -2,6 +2,8 @@ import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
+import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
+import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster';
@@ -45,6 +47,8 @@ export const metadata = {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getServerComponentAllFlags();
+ const locale = getLocale();
+
return (
-
-
-
-
- {children}
-
-
-
-
-
+
+
+
+
+
+ {children}
+
+
+
+
+
+
);
diff --git a/apps/web/src/components/formatter/locale-date.tsx b/apps/web/src/components/formatter/locale-date.tsx
index 837c6aa38..ecefb1e3b 100644
--- a/apps/web/src/components/formatter/locale-date.tsx
+++ b/apps/web/src/components/formatter/locale-date.tsx
@@ -2,16 +2,31 @@
import { HTMLAttributes, useEffect, useState } from 'react';
+import { DateTime, DateTimeFormatOptions } from 'luxon';
+
+import { useLocale } from '@documenso/lib/client-only/providers/locale';
+
export type LocaleDateProps = HTMLAttributes & {
date: string | number | Date;
+ format?: DateTimeFormatOptions;
};
-export const LocaleDate = ({ className, date, ...props }: LocaleDateProps) => {
- const [localeDate, setLocaleDate] = useState(() => new Date(date).toISOString());
+/**
+ * Formats the date based on the user locale.
+ *
+ * Will use the estimated locale from the user headers on SSR, then will use
+ * the client browser locale once mounted.
+ */
+export const LocaleDate = ({ className, date, format, ...props }: LocaleDateProps) => {
+ const { locale } = useLocale();
+
+ const [localeDate, setLocaleDate] = useState(() =>
+ DateTime.fromJSDate(new Date(date)).setLocale(locale).toLocaleString(format),
+ );
useEffect(() => {
- setLocaleDate(new Date(date).toLocaleString());
- }, [date]);
+ setLocaleDate(DateTime.fromJSDate(new Date(date)).toLocaleString(format));
+ }, [date, format]);
return (
diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx
index 5e44146ea..d9d727afc 100644
--- a/apps/web/src/components/forms/signin.tsx
+++ b/apps/web/src/components/forms/signin.tsx
@@ -18,13 +18,15 @@ import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
-const ErrorMessages = {
+const ERROR_MESSAGES = {
[ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect',
[ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'The email or password provided is incorrect',
[ErrorCode.USER_MISSING_PASSWORD]:
'This account appears to be using a social login method, please sign in using that method',
};
+const LOGIN_REDIRECT_PATH = '/documents';
+
export const ZSignInFormSchema = z.object({
email: z.string().email().min(1),
password: z.string().min(6).max(72),
@@ -37,9 +39,10 @@ export type SignInFormProps = {
};
export const SignInForm = ({ className }: SignInFormProps) => {
- const { toast } = useToast();
const searchParams = useSearchParams();
+ const { toast } = useToast();
+
const {
register,
handleSubmit,
@@ -61,7 +64,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
timeout = setTimeout(() => {
toast({
variant: 'destructive',
- description: ErrorMessages[errorCode] ?? 'An unknown error occurred',
+ description: ERROR_MESSAGES[errorCode] ?? 'An unknown error occurred',
});
}, 0);
}
@@ -78,12 +81,10 @@ export const SignInForm = ({ className }: SignInFormProps) => {
await signIn('credentials', {
email,
password,
- callbackUrl: '/documents',
+ callbackUrl: LOGIN_REDIRECT_PATH,
}).catch((err) => {
console.error(err);
});
-
- // throw new Error('Not implemented');
} catch (err) {
toast({
title: 'An unknown error occurred',
@@ -95,8 +96,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
const onSignInWithGoogleClick = async () => {
try {
- await signIn('google', { callbackUrl: '/dashboard' });
- // throw new Error('Not implemented');
+ await signIn('google', { callbackUrl: LOGIN_REDIRECT_PATH });
} catch (err) {
toast({
title: 'An unknown error occurred',
diff --git a/packages/lib/client-only/providers/locale.tsx b/packages/lib/client-only/providers/locale.tsx
new file mode 100644
index 000000000..ff8b03e5a
--- /dev/null
+++ b/packages/lib/client-only/providers/locale.tsx
@@ -0,0 +1,37 @@
+'use client';
+
+import { createContext, useContext } from 'react';
+
+export type LocaleContextValue = {
+ locale: string;
+};
+
+export const LocaleContext = createContext(null);
+
+export const useLocale = () => {
+ const context = useContext(LocaleContext);
+
+ if (!context) {
+ throw new Error('useLocale must be used within a LocaleProvider');
+ }
+
+ return context;
+};
+
+export function LocaleProvider({
+ children,
+ locale,
+}: {
+ children: React.ReactNode;
+ locale: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/ui/primitives/data-table-pagination.tsx b/packages/ui/primitives/data-table-pagination.tsx
index 0ff27ae11..8147c92fb 100644
--- a/packages/ui/primitives/data-table-pagination.tsx
+++ b/packages/ui/primitives/data-table-pagination.tsx
@@ -1,19 +1,46 @@
import { Table } from '@tanstack/react-table';
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
+import { match } from 'ts-pattern';
import { Button } from './button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';
interface DataTablePaginationProps {
table: Table;
+
+ /**
+ * The type of information to show on the left hand side of the pagination.
+ *
+ * Defaults to 'VisibleCount'.
+ */
+ additionalInformation?: 'SelectedCount' | 'VisibleCount' | 'None';
}
-export function DataTablePagination({ table }: DataTablePaginationProps) {
+export function DataTablePagination({
+ table,
+ additionalInformation = 'VisibleCount',
+}: DataTablePaginationProps) {
return (
- {table.getFilteredSelectedRowModel().rows.length} of{' '}
- {table.getFilteredRowModel().rows.length} row(s) selected.
+ {match(additionalInformation)
+ .with('SelectedCount', () => (
+
+ {table.getFilteredSelectedRowModel().rows.length} of{' '}
+ {table.getFilteredRowModel().rows.length} row(s) selected.
+
+ ))
+ .with('VisibleCount', () => {
+ const visibleRows = table.getFilteredRowModel().rows.length;
+
+ return (
+
+ Showing {visibleRows} result{visibleRows > 1 && 's'}.
+
+ );
+ })
+ .with('None', () => null)
+ .exhaustive()}