diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..59a318b7f
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,20 @@
+{
+ "name": "Documenso",
+ "image": "mcr.microsoft.com/devcontainers/base:bullseye",
+ "features": {
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {
+ "version": "latest",
+ "enableNonRootDocker": "true",
+ "moby": "true"
+ },
+ "ghcr.io/devcontainers/features/node:1": {}
+ },
+ "onCreateCommand": "./.devcontainer/on-create.sh",
+ "forwardPorts": [
+ 3000,
+ 54320,
+ 9000,
+ 2500,
+ 1100
+ ]
+}
diff --git a/.devcontainer/on-create.sh b/.devcontainer/on-create.sh
new file mode 100755
index 000000000..a66491ef7
--- /dev/null
+++ b/.devcontainer/on-create.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+# Start the database and mailserver
+docker compose -f ./docker/compose-without-app.yml up -d
+
+# Install dependencies
+npm install
+
+# Copy the env file
+cp .env.example .env
+
+# Source the env file, export the variables
+set -a
+source .env
+set +a
+
+# Run the migrations
+npm run -w @documenso/prisma prisma:migrate-dev
diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh
new file mode 100755
index 000000000..80d19dc7c
--- /dev/null
+++ b/.devcontainer/post-start.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+npm run dev
diff --git a/apps/marketing/content/blog/next.mdx b/apps/marketing/content/blog/next.mdx
index 4f846a0a7..c241cf3eb 100644
--- a/apps/marketing/content/blog/next.mdx
+++ b/apps/marketing/content/blog/next.mdx
@@ -12,7 +12,7 @@ tags:
Since we launched [Documenso 0.9 on Product Hunt](https://producthunt.com/products/documenso#documenso) last May, the team's been hard at work behind the scenes to ramp up development and design to deliver an excellent next version.
-Last week, Lucas shared the reasoning how [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite).
+Last week, Lucas shared the reasoning on [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite).
Today, I'm pleased to share with you a preview of the next Documenso.
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 653936b9a..0ea19cfc4 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
+++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
@@ -139,7 +139,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-title.tsx b/apps/web/src/app/(dashboard)/documents/data-table-title.tsx
new file mode 100644
index 000000000..c04f9f13d
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/documents/data-table-title.tsx
@@ -0,0 +1,56 @@
+'use client';
+
+import Link from 'next/link';
+
+import { useSession } from 'next-auth/react';
+import { match } from 'ts-pattern';
+
+import { Document, Recipient, User } from '@documenso/prisma/client';
+
+export type DataTableTitleProps = {
+ row: Document & {
+ User: Pick;
+ Recipient: Recipient[];
+ };
+};
+
+export const DataTableTitle = ({ row }: DataTableTitleProps) => {
+ const { data: session } = useSession();
+
+ if (!session) {
+ return null;
+ }
+
+ const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
+
+ const isOwner = row.User.id === session.user.id;
+ const isRecipient = !!recipient;
+
+ return match({
+ isOwner,
+ isRecipient,
+ })
+ .with({ isOwner: true }, () => (
+
+ {row.title}
+
+ ))
+ .with({ isRecipient: true }, () => (
+
+ {row.title}
+
+ ))
+ .otherwise(() => (
+
+ {row.title}
+
+ ));
+};
diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx
index 1d6c08e73..b8c735b59 100644
--- a/apps/web/src/app/(dashboard)/documents/data-table.tsx
+++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx
@@ -2,9 +2,8 @@
import { useTransition } from 'react';
-import Link from 'next/link';
-
import { Loader } from 'lucide-react';
+import { useSession } from 'next-auth/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { FindResultSet } from '@documenso/lib/types/find-result-set';
@@ -18,6 +17,7 @@ import { LocaleDate } from '~/components/formatter/locale-date';
import { DataTableActionButton } from './data-table-action-button';
import { DataTableActionDropdown } from './data-table-action-dropdown';
+import { DataTableTitle } from './data-table-title';
export type DocumentsDataTableProps = {
results: FindResultSet<
@@ -29,6 +29,7 @@ export type DocumentsDataTableProps = {
};
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
+ const { data: session } = useSession();
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
@@ -42,25 +43,22 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
});
};
+ if (!session) {
+ return null;
+ }
+
return (
,
},
{
header: 'Title',
- cell: ({ row }) => (
-
- {row.original.title}
-
- ),
+ cell: ({ row }) => ,
},
{
header: 'Recipient',
@@ -74,11 +72,6 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
accessorKey: 'status',
cell: ({ row }) => ,
},
- {
- header: 'Created',
- accessorKey: 'created',
- cell: ({ row }) => ,
- },
{
header: 'Actions',
cell: ({ row }) => (
@@ -95,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 4ea55936b..d1f558806 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?: {
@@ -81,6 +81,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
{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/package.json b/package.json
index b66c194a2..3b2fbf6ca 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"private": true,
"scripts": {
"build": "turbo run build",
- "dev": "turbo run dev --filter=@documenso/{web,marketing}",
+ "dev": "turbo run dev --filter=@documenso/web --filter=@documenso/marketing",
"start": "cd apps && cd web && next start",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
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()}
diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx
index c56e91b49..5de43c411 100644
--- a/packages/ui/primitives/document-flow/add-fields.tsx
+++ b/packages/ui/primitives/document-flow/add-fields.tsx
@@ -102,6 +102,7 @@ export const AddFieldsFormPartial = ({
const [selectedField, setSelectedField] = useState
(null);
const [selectedSigner, setSelectedSigner] = useState(null);
+ const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
@@ -314,7 +315,7 @@ export const AddFieldsFormPartial = ({
))}
{!hideRecipients && (
-
+