;
-export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProps) => {
+export const DocumentSigningDisclosure = ({
+ className,
+ ...props
+}: DocumentSigningDisclosureProps) => {
return (
@@ -22,7 +24,7 @@ export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProp
Read the full{' '}
signature disclosure
diff --git a/apps/web/src/app/(signing)/sign/[token]/dropdown-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-dropdown-field.tsx
similarity index 88%
rename from apps/web/src/app/(signing)/sign/[token]/dropdown-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-dropdown-field.tsx
index 5f4e1a444..b34976a79 100644
--- a/apps/web/src/app/(signing)/sign/[token]/dropdown-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-dropdown-field.tsx
@@ -1,18 +1,15 @@
-'use client';
+import { useEffect, useState } from 'react';
-import { useEffect, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import type { Recipient } from '@prisma/client';
import { Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZDropdownFieldMeta } from '@documenso/lib/types/field-meta';
-import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -29,29 +26,27 @@ import {
} from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
-export type DropdownFieldProps = {
+export type DocumentSigningDropdownFieldProps = {
field: FieldWithSignatureAndFieldMeta;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const DropdownField = ({
+export const DocumentSigningDropdownField = ({
field,
recipient,
onSignField,
onUnsignField,
-}: DropdownFieldProps) => {
+}: DocumentSigningDropdownFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
+ const { revalidate } = useRevalidator();
- const router = useRouter();
- const [isPending, startTransition] = useTransition();
-
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
+ const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
const isReadOnly = parsedFieldMeta?.readOnly;
@@ -66,7 +61,7 @@ export const DropdownField = ({
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const shouldAutoSignField =
(!field.inserted && localChoice) || (!field.inserted && isReadOnly && defaultValue);
@@ -91,7 +86,8 @@ export const DropdownField = ({
}
setLocalChoice('');
- startTransition(() => router.refresh());
+
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -128,7 +124,8 @@ export const DropdownField = ({
}
setLocalChoice('');
- startTransition(() => router.refresh());
+
+ await revalidate();
} catch (err) {
console.error(err);
@@ -164,7 +161,7 @@ export const DropdownField = ({
return (
-
)}
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/email-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
similarity index 79%
rename from apps/web/src/app/(signing)/sign/[token]/email-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
index 9300aef63..4f4b16d16 100644
--- a/apps/web/src/app/(signing)/sign/[token]/email-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
@@ -1,17 +1,13 @@
-'use client';
-
-import { useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { Recipient } from '@prisma/client';
import { Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -20,25 +16,27 @@ import type {
} from '@documenso/trpc/server/field-router/schema';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredSigningContext } from './provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
+import { useRequiredDocumentSigningContext } from './document-signing-provider';
-export type EmailFieldProps = {
+export type DocumentSigningEmailFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const EmailField = ({ field, recipient, onSignField, onUnsignField }: EmailFieldProps) => {
- const router = useRouter();
-
+export const DocumentSigningEmailField = ({
+ field,
+ recipient,
+ onSignField,
+ onUnsignField,
+}: DocumentSigningEmailFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
+ const { revalidate } = useRevalidator();
- const { email: providedEmail } = useRequiredSigningContext();
-
- const [isPending, startTransition] = useTransition();
+ const { email: providedEmail } = useRequiredDocumentSigningContext();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -48,7 +46,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const onSign = async (authOptions?: TRecipientActionAuth) => {
try {
@@ -69,7 +67,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
await signFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -101,7 +99,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
await removeSignedFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
@@ -114,7 +112,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
};
return (
-
+
{isLoading && (
@@ -132,6 +130,6 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
{field.customText}
)}
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx b/apps/remix/app/components/general/document-signing/document-signing-field-container.tsx
similarity index 93%
rename from apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-field-container.tsx
index cf8403696..111c16c6f 100644
--- a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-field-container.tsx
@@ -1,21 +1,19 @@
-'use client';
-
import React from 'react';
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
+import { FieldType } from '@prisma/client';
import { X } from 'lucide-react';
import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
-import { FieldType } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { cn } from '@documenso/ui/lib/utils';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
-export type SignatureFieldProps = {
+export type DocumentSigningFieldContainerProps = {
field: FieldWithSignature;
loading?: boolean;
children: React.ReactNode;
@@ -53,7 +51,7 @@ export type SignatureFieldProps = {
tooltipText?: string | null;
};
-export const SigningFieldContainer = ({
+export const DocumentSigningFieldContainer = ({
field,
loading,
onPreSign,
@@ -62,8 +60,9 @@ export const SigningFieldContainer = ({
children,
type,
tooltipText,
-}: SignatureFieldProps) => {
- const { executeActionAuthProcedure, isAuthRedirectRequired } = useRequiredDocumentAuthContext();
+}: DocumentSigningFieldContainerProps) => {
+ const { executeActionAuthProcedure, isAuthRedirectRequired } =
+ useRequiredDocumentSigningAuthContext();
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
const readOnlyField = parsedFieldMeta?.readOnly || false;
diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/remix/app/components/general/document-signing/document-signing-form.tsx
similarity index 89%
rename from apps/web/src/app/(signing)/sign/[token]/form.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-form.tsx
index b69280c71..4e971b7b4 100644
--- a/apps/web/src/app/(signing)/sign/[token]/form.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-form.tsx
@@ -1,19 +1,16 @@
-'use client';
-
import { useMemo, useState } from 'react';
-import { useRouter } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { useSession } from 'next-auth/react';
+import { Trans } from '@lingui/react/macro';
+import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
import { useForm } from 'react-hook-form';
+import { useNavigate } from 'react-router';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
+import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
-import { type Field, FieldType, type Recipient, RecipientRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
@@ -23,10 +20,10 @@ import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
-import { useRequiredSigningContext } from './provider';
-import { SignDialog } from './sign-dialog';
+import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
+import { useRequiredDocumentSigningContext } from './document-signing-provider';
-export type SigningFormProps = {
+export type DocumentSigningFormProps = {
document: DocumentAndSender;
recipient: Recipient;
fields: Field[];
@@ -34,19 +31,20 @@ export type SigningFormProps = {
isRecipientsTurn: boolean;
};
-export const SigningForm = ({
+export const DocumentSigningForm = ({
document,
recipient,
fields,
redirectUrl,
isRecipientsTurn,
-}: SigningFormProps) => {
- const router = useRouter();
+}: DocumentSigningFormProps) => {
+ const navigate = useNavigate();
const analytics = useAnalytics();
- const { data: session } = useSession();
+
+ const { user } = useOptionalSession();
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
- useRequiredSigningContext();
+ useRequiredDocumentSigningContext();
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
@@ -109,7 +107,11 @@ export const SigningForm = ({
timestamp: new Date().toISOString(),
});
- redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`);
+ if (redirectUrl) {
+ window.location.href = redirectUrl;
+ } else {
+ await navigate(`/sign/${recipient.token}/complete`);
+ }
};
return (
@@ -117,8 +119,8 @@ export const SigningForm = ({
className={cn(
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
{
- 'top-20 max-h-[min(68rem,calc(100vh-6rem))]': session,
- 'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !session,
+ 'top-20 max-h-[min(68rem,calc(100vh-6rem))]': user,
+ 'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !user,
},
)}
onSubmit={handleSubmit(onFormSubmit)}
@@ -159,12 +161,12 @@ export const SigningForm = ({
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
- onClick={() => router.back()}
+ onClick={async () => navigate(-1)}
>
Cancel
-
router.back()}
+ onClick={async () => navigate(-1)}
>
Cancel
- Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const InitialsField = ({
+export const DocumentSigningInitialsField = ({
field,
recipient,
onSignField,
onUnsignField,
-}: InitialsFieldProps) => {
- const router = useRouter();
+}: DocumentSigningInitialsFieldProps) => {
const { toast } = useToast();
const { _ } = useLingui();
+ const { revalidate } = useRevalidator();
- const { fullName } = useRequiredSigningContext();
+ const { fullName } = useRequiredDocumentSigningContext();
const initials = extractInitials(fullName);
- const [isPending, startTransition] = useTransition();
-
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -54,7 +48,7 @@ export const InitialsField = ({
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const onSign = async (authOptions?: TRecipientActionAuth) => {
try {
@@ -75,7 +69,7 @@ export const InitialsField = ({
await signFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -107,7 +101,7 @@ export const InitialsField = ({
await removeSignedFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
@@ -120,7 +114,12 @@ export const InitialsField = ({
};
return (
-
+
{isLoading && (
@@ -138,6 +137,6 @@ export const InitialsField = ({
{field.customText}
)}
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/name-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
similarity index 86%
rename from apps/web/src/app/(signing)/sign/[token]/name-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
index bc83e5a49..3a97c4dca 100644
--- a/apps/web/src/app/(signing)/sign/[token]/name-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
@@ -1,17 +1,15 @@
-'use client';
+import { useState } from 'react';
-import { useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { type Recipient } from '@prisma/client';
import { Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { type Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -24,29 +22,31 @@ import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { useRequiredSigningContext } from './provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
+import { useRequiredDocumentSigningContext } from './document-signing-provider';
-export type NameFieldProps = {
+export type DocumentSigningNameFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const NameField = ({ field, recipient, onSignField, onUnsignField }: NameFieldProps) => {
- const router = useRouter();
-
+export const DocumentSigningNameField = ({
+ field,
+ recipient,
+ onSignField,
+ onUnsignField,
+}: DocumentSigningNameFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
+ const { revalidate } = useRevalidator();
const { fullName: providedFullName, setFullName: setProvidedFullName } =
- useRequiredSigningContext();
+ useRequiredDocumentSigningContext();
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
-
- const [isPending, startTransition] = useTransition();
+ const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -56,7 +56,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const [showFullNameModal, setShowFullNameModal] = useState(false);
const [localFullName, setLocalFullName] = useState('');
@@ -107,7 +107,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
await signFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -139,7 +139,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
await removeSignedFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
@@ -152,7 +152,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
};
return (
-
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/number-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
similarity index 92%
rename from apps/web/src/app/(signing)/sign/[token]/number-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
index ffd90df64..78b474e29 100644
--- a/apps/web/src/app/(signing)/sign/[token]/number-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
@@ -1,19 +1,17 @@
-'use client';
+import { useEffect, useState } from 'react';
-import { useEffect, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { Recipient } from '@prisma/client';
import { Hash, Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZNumberFieldMeta } from '@documenso/lib/types/field-meta';
-import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -26,8 +24,8 @@ import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
type ValidationErrors = {
isNumber: string[];
@@ -37,19 +35,23 @@ type ValidationErrors = {
numberFormat: string[];
};
-export type NumberFieldProps = {
+export type DocumentSigningNumberFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const NumberField = ({ field, recipient, onSignField, onUnsignField }: NumberFieldProps) => {
+export const DocumentSigningNumberField = ({
+ field,
+ recipient,
+ onSignField,
+ onUnsignField,
+}: DocumentSigningNumberFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
+ const { revalidate } = useRevalidator();
- const router = useRouter();
- const [isPending, startTransition] = useTransition();
const [showRadioModal, setShowRadioModal] = useState(false);
const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null;
@@ -69,7 +71,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
const [errors, setErrors] = useState(initialErrors);
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
+ const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -79,7 +81,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const handleNumberChange = (e: React.ChangeEvent) => {
const text = e.target.value;
@@ -135,7 +137,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
setLocalNumber('');
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -186,7 +188,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta?.value) : '');
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
@@ -229,7 +231,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
return (
-
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx b/apps/remix/app/components/general/document-signing/document-signing-page-view.tsx
similarity index 66%
rename from apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-page-view.tsx
index 019f3e9c3..1057311b9 100644
--- a/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-page-view.tsx
@@ -1,4 +1,6 @@
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
+import type { Field, Recipient } from '@prisma/client';
+import { FieldType, RecipientRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@@ -13,28 +15,25 @@ import {
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import type { CompletedField } from '@documenso/lib/types/fields';
-import type { Field, Recipient } from '@documenso/prisma/client';
-import { FieldType, RecipientRole } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
-
-import { AutoSign } from './auto-sign';
-import { CheckboxField } from './checkbox-field';
-import { DateField } from './date-field';
-import { DropdownField } from './dropdown-field';
-import { EmailField } from './email-field';
-import { SigningForm } from './form';
-import { InitialsField } from './initials-field';
-import { NameField } from './name-field';
-import { NumberField } from './number-field';
-import { RadioField } from './radio-field';
-import { RejectDocumentDialog } from './reject-document-dialog';
-import { SignatureField } from './signature-field';
-import { TextField } from './text-field';
+import { DocumentSigningAutoSign } from '~/components/general/document-signing/document-signing-auto-sign';
+import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
+import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
+import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field';
+import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field';
+import { DocumentSigningForm } from '~/components/general/document-signing/document-signing-form';
+import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field';
+import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field';
+import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field';
+import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
+import { DocumentSigningRejectDialog } from '~/components/general/document-signing/document-signing-reject-dialog';
+import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
+import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
+import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
export type SigningPageViewProps = {
document: DocumentAndSender;
@@ -44,7 +43,7 @@ export type SigningPageViewProps = {
isRecipientsTurn: boolean;
};
-export const SigningPageView = ({
+export const DocumentSigningPageView = ({
document,
recipient,
fields,
@@ -111,7 +110,7 @@ export const SigningPageView = ({
-
+
@@ -130,7 +129,7 @@ export const SigningPageView = ({
-
-
+
{fields.map((field) =>
match(field.type)
.with(FieldType.SIGNATURE, () => (
-
))
.with(FieldType.INITIALS, () => (
-
+
))
.with(FieldType.NAME, () => (
-
+
))
.with(FieldType.DATE, () => (
-
))
.with(FieldType.EMAIL, () => (
-
+
))
.with(FieldType.TEXT, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null,
};
- return ;
+ return (
+
+ );
})
.with(FieldType.NUMBER, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null,
};
- return ;
+ return (
+
+ );
})
.with(FieldType.RADIO, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null,
};
- return ;
+ return (
+
+ );
})
.with(FieldType.CHECKBOX, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null,
};
- return ;
+ return (
+
+ );
})
.with(FieldType.DROPDOWN, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null,
};
- return ;
+ return (
+
+ );
})
.otherwise(() => null),
)}
diff --git a/apps/web/src/app/(signing)/sign/[token]/provider.tsx b/apps/remix/app/components/general/document-signing/document-signing-provider.tsx
similarity index 67%
rename from apps/web/src/app/(signing)/sign/[token]/provider.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-provider.tsx
index 3e491bf32..ca231949d 100644
--- a/apps/web/src/app/(signing)/sign/[token]/provider.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-provider.tsx
@@ -1,8 +1,6 @@
-'use client';
-
import { createContext, useContext, useEffect, useState } from 'react';
-export type SigningContextValue = {
+export type DocumentSigningContextValue = {
fullName: string;
setFullName: (_value: string) => void;
email: string;
@@ -13,14 +11,14 @@ export type SigningContextValue = {
setSignatureValid: (_valid: boolean) => void;
};
-const SigningContext = createContext(null);
+const DocumentSigningContext = createContext(null);
-export const useSigningContext = () => {
- return useContext(SigningContext);
+export const useDocumentSigningContext = () => {
+ return useContext(DocumentSigningContext);
};
-export const useRequiredSigningContext = () => {
- const context = useSigningContext();
+export const useRequiredDocumentSigningContext = () => {
+ const context = useDocumentSigningContext();
if (!context) {
throw new Error('Signing context is required');
@@ -29,19 +27,19 @@ export const useRequiredSigningContext = () => {
return context;
};
-export interface SigningProviderProps {
+export interface DocumentSigningProviderProps {
fullName?: string | null;
email?: string | null;
signature?: string | null;
children: React.ReactNode;
}
-export const SigningProvider = ({
+export const DocumentSigningProvider = ({
fullName: initialFullName,
email: initialEmail,
signature: initialSignature,
children,
-}: SigningProviderProps) => {
+}: DocumentSigningProviderProps) => {
const [fullName, setFullName] = useState(initialFullName || '');
const [email, setEmail] = useState(initialEmail || '');
const [signature, setSignature] = useState(initialSignature || null);
@@ -54,7 +52,7 @@ export const SigningProvider = ({
}, [initialSignature]);
return (
-
{children}
-
+
);
};
-SigningProvider.displayName = 'SigningProvider';
+DocumentSigningProvider.displayName = 'DocumentSigningProvider';
diff --git a/apps/web/src/app/(signing)/sign/[token]/radio-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-radio-field.tsx
similarity index 86%
rename from apps/web/src/app/(signing)/sign/[token]/radio-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-radio-field.tsx
index 398181ec1..61c2771c9 100644
--- a/apps/web/src/app/(signing)/sign/[token]/radio-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-radio-field.tsx
@@ -1,18 +1,15 @@
-'use client';
+import { useEffect, useState } from 'react';
-import { useEffect, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import type { Recipient } from '@prisma/client';
import { Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
-import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -23,22 +20,25 @@ import { Label } from '@documenso/ui/primitives/label';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
-export type RadioFieldProps = {
+export type DocumentSigningRadioFieldProps = {
field: FieldWithSignatureAndFieldMeta;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const RadioField = ({ field, recipient, onSignField, onUnsignField }: RadioFieldProps) => {
+export const DocumentSigningRadioField = ({
+ field,
+ recipient,
+ onSignField,
+ onUnsignField,
+}: DocumentSigningRadioFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
-
- const router = useRouter();
- const [isPending, startTransition] = useTransition();
+ const { revalidate } = useRevalidator();
const parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
const values = parsedFieldMeta.values?.map((item) => ({
@@ -50,7 +50,7 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
const [selectedOption, setSelectedOption] = useState(defaultValue);
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
+ const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -60,7 +60,7 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const shouldAutoSignField =
(!field.inserted && selectedOption) ||
(!field.inserted && defaultValue) ||
@@ -87,7 +87,8 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
}
setSelectedOption('');
- startTransition(() => router.refresh());
+
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -120,7 +121,7 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
setSelectedOption('');
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
@@ -146,7 +147,7 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
}, [selectedOption, field]);
return (
-
+
{isLoading && (
@@ -189,6 +190,6 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
))}
)}
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx b/apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx
similarity index 88%
rename from apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx
index 547a346d8..1160f89b8 100644
--- a/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx
@@ -1,15 +1,14 @@
-'use client';
-
import { useEffect, useState } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
-
import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
+import { Trans } from '@lingui/react/macro';
+import type { Document } from '@prisma/client';
import { useForm } from 'react-hook-form';
+import { useNavigate } from 'react-router';
+import { useSearchParams } from 'react-router';
import { z } from 'zod';
-import type { Document } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
@@ -40,15 +39,15 @@ const ZRejectDocumentFormSchema = z.object({
type TRejectDocumentFormSchema = z.infer
;
-export interface RejectDocumentDialogProps {
+export interface DocumentSigningRejectDialogProps {
document: Pick;
token: string;
}
-export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) {
+export function DocumentSigningRejectDialog({ document, token }: DocumentSigningRejectDialogProps) {
const { toast } = useToast();
- const router = useRouter();
- const searchParams = useSearchParams();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
const [isOpen, setIsOpen] = useState(false);
@@ -64,7 +63,6 @@ export function RejectDocumentDialog({ document, token }: RejectDocumentDialogPr
const onRejectDocument = async ({ reason }: TRejectDocumentFormSchema) => {
try {
- // TODO: Add trpc mutation here
await rejectDocumentWithToken({
documentId: document.id,
token,
@@ -77,9 +75,9 @@ export function RejectDocumentDialog({ document, token }: RejectDocumentDialogPr
duration: 5000,
});
- setIsOpen(false);
+ await navigate(`/sign/${token}/rejected`);
- router.push(`/sign/${token}/rejected`);
+ setIsOpen(false);
} catch (err) {
toast({
title: 'Error',
diff --git a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
similarity index 88%
rename from apps/web/src/app/(signing)/sign/[token]/signature-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
index bba784975..99d29c2d5 100644
--- a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
@@ -1,17 +1,15 @@
-'use client';
+import { useLayoutEffect, useMemo, useRef, useState } from 'react';
-import { useLayoutEffect, useMemo, useRef, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { type Recipient } from '@prisma/client';
import { Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { type Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -24,14 +22,14 @@ import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { SigningDisclosure } from '~/components/general/signing-disclosure';
+import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { useRequiredSigningContext } from './provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
+import { useRequiredDocumentSigningContext } from './document-signing-provider';
type SignatureFieldState = 'empty' | 'signed-image' | 'signed-text';
-export type SignatureFieldProps = {
+export type DocumentSigningSignatureFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
@@ -39,17 +37,16 @@ export type SignatureFieldProps = {
typedSignatureEnabled?: boolean;
};
-export const SignatureField = ({
+export const DocumentSigningSignatureField = ({
field,
recipient,
onSignField,
onUnsignField,
typedSignatureEnabled,
-}: SignatureFieldProps) => {
- const router = useRouter();
-
+}: DocumentSigningSignatureFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
+ const { revalidate } = useRevalidator();
const signatureRef = useRef(null);
const containerRef = useRef(null);
@@ -60,11 +57,9 @@ export const SignatureField = ({
setSignature: setProvidedSignature,
signatureValid,
setSignatureValid,
- } = useRequiredSigningContext();
+ } = useRequiredDocumentSigningContext();
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
-
- const [isPending, startTransition] = useTransition();
+ const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -76,7 +71,7 @@ export const SignatureField = ({
const { signature } = field;
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const [showSignatureModal, setShowSignatureModal] = useState(false);
const [localSignature, setLocalSignature] = useState(null);
@@ -148,12 +143,11 @@ export const SignatureField = ({
if (onSignField) {
await onSignField(payload);
- return;
+ } else {
+ await signFieldWithToken(payload);
}
- await signFieldWithToken(payload);
-
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -181,11 +175,11 @@ export const SignatureField = ({
if (onUnsignField) {
await onUnsignField(payload);
return;
+ } else {
+ await removeSignedFieldWithToken(payload);
}
- await removeSignedFieldWithToken(payload);
-
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
@@ -233,7 +227,7 @@ export const SignatureField = ({
}, [signature?.typedSignature]);
return (
-
-
+
+
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/text-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
similarity index 92%
rename from apps/web/src/app/(signing)/sign/[token]/text-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
index 0c4088d75..466bcef1d 100644
--- a/apps/web/src/app/(signing)/sign/[token]/text-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
@@ -1,19 +1,17 @@
-'use client';
+import { useEffect, useState } from 'react';
-import { useEffect, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Plural, Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Plural, Trans } from '@lingui/react/macro';
+import type { Recipient } from '@prisma/client';
import { Loader, Type } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZTextFieldMeta } from '@documenso/lib/types/field-meta';
-import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -26,21 +24,25 @@ import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/
import { Textarea } from '@documenso/ui/primitives/textarea';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
-export type TextFieldProps = {
+export type DocumentSigningTextFieldProps = {
field: FieldWithSignatureAndFieldMeta;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const TextField = ({ field, recipient, onSignField, onUnsignField }: TextFieldProps) => {
+export const DocumentSigningTextField = ({
+ field,
+ recipient,
+ onSignField,
+ onUnsignField,
+}: DocumentSigningTextFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
-
- const router = useRouter();
+ const { revalidate } = useRevalidator();
const initialErrors: Record = {
required: [],
@@ -50,9 +52,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
const [errors, setErrors] = useState(initialErrors);
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
-
- const [isPending, startTransition] = useTransition();
+ const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -64,7 +64,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
const parsedFieldMeta = field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null;
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const shouldAutoSignField =
(!field.inserted && parsedFieldMeta?.text) ||
(!field.inserted && parsedFieldMeta?.text && parsedFieldMeta?.readOnly);
@@ -153,7 +153,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
setLocalCustomText('');
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -187,7 +187,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
setLocalCustomText(parsedFieldMeta?.text ?? '');
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
@@ -228,7 +228,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
const charactersRemaining = (parsedFieldMeta?.characterLimit ?? 0) - (localText.length ?? 0);
return (
-
-
+
);
};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/download-audit-log-button.tsx b/apps/remix/app/components/general/document/document-audit-log-download-button.tsx
similarity index 88%
rename from apps/web/src/app/(dashboard)/documents/[id]/logs/download-audit-log-button.tsx
rename to apps/remix/app/components/general/document/document-audit-log-download-button.tsx
index d6be5318c..fb531eb37 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/logs/download-audit-log-button.tsx
+++ b/apps/remix/app/components/general/document/document-audit-log-download-button.tsx
@@ -1,7 +1,6 @@
-'use client';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { DownloadIcon } from 'lucide-react';
import { trpc } from '@documenso/trpc/react';
@@ -9,13 +8,15 @@ import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
-export type DownloadAuditLogButtonProps = {
+export type DocumentAuditLogDownloadButtonProps = {
className?: string;
- teamId?: number;
documentId: number;
};
-export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
+export const DocumentAuditLogDownloadButton = ({
+ className,
+ documentId,
+}: DocumentAuditLogDownloadButtonProps) => {
const { toast } = useToast();
const { _ } = useLingui();
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/download-certificate-button.tsx b/apps/remix/app/components/general/document/document-certificate-download-button.tsx
similarity index 88%
rename from apps/web/src/app/(dashboard)/documents/[id]/logs/download-certificate-button.tsx
rename to apps/remix/app/components/general/document/document-certificate-download-button.tsx
index 18eff7258..bdf2d2ac9 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/logs/download-certificate-button.tsx
+++ b/apps/remix/app/components/general/document/document-certificate-download-button.tsx
@@ -1,28 +1,25 @@
-'use client';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { DocumentStatus } from '@prisma/client';
import { DownloadIcon } from 'lucide-react';
-import { DocumentStatus } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
-export type DownloadCertificateButtonProps = {
+export type DocumentCertificateDownloadButtonProps = {
className?: string;
documentId: number;
documentStatus: DocumentStatus;
- teamId?: number;
};
-export const DownloadCertificateButton = ({
+export const DocumentCertificateDownloadButton = ({
className,
documentId,
documentStatus,
- teamId,
-}: DownloadCertificateButtonProps) => {
+}: DocumentCertificateDownloadButtonProps) => {
const { toast } = useToast();
const { _ } = useLingui();
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/remix/app/components/general/document/document-edit-form.tsx
similarity index 93%
rename from apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
rename to apps/remix/app/components/general/document/document-edit-form.tsx
index 7977aa2c6..df9d1bd8d 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
+++ b/apps/remix/app/components/general/document/document-edit-form.tsx
@@ -1,11 +1,9 @@
-'use client';
-
import { useEffect, useState } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
+import { useNavigate, useSearchParams } from 'react-router';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import {
@@ -13,7 +11,6 @@ import {
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import type { TDocument } from '@documenso/lib/types/document';
-import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@@ -33,7 +30,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
-export type EditDocumentFormProps = {
+export type DocumentEditFormProps = {
className?: string;
initialDocument: TDocument;
documentRootPath: string;
@@ -43,17 +40,18 @@ export type EditDocumentFormProps = {
type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
const EditDocumentSteps: EditDocumentStep[] = ['settings', 'signers', 'fields', 'subject'];
-export const EditDocumentForm = ({
+export const DocumentEditForm = ({
className,
initialDocument,
documentRootPath,
isDocumentEnterprise,
-}: EditDocumentFormProps) => {
+}: DocumentEditFormProps) => {
const { toast } = useToast();
const { _ } = useLingui();
- const router = useRouter();
- const searchParams = useSearchParams();
+ const navigate = useNavigate();
+
+ const [searchParams] = useSearchParams();
const team = useOptionalCurrentTeam();
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
@@ -198,9 +196,6 @@ export const EditDocumentForm = ({
},
});
- // Router refresh is here to clear the router cache for when navigating to /documents.
- router.refresh();
-
setStep('signers');
} catch (err) {
console.error(err);
@@ -231,9 +226,6 @@ export const EditDocumentForm = ({
}),
]);
- // Router refresh is here to clear the router cache for when navigating to /documents.
- router.refresh();
-
setStep('fields');
} catch (err) {
console.error(err);
@@ -269,9 +261,6 @@ export const EditDocumentForm = ({
}
}
- // Router refresh is here to clear the router cache for when navigating to /documents.
- router.refresh();
-
setStep('subject');
} catch (err) {
console.error(err);
@@ -305,18 +294,15 @@ export const EditDocumentForm = ({
duration: 5000,
});
- router.push(documentRootPath);
- return;
- }
-
- if (document.status === DocumentStatus.DRAFT) {
+ await navigate(documentRootPath);
+ } else if (document.status === DocumentStatus.DRAFT) {
toast({
title: _(msg`Links Generated`),
description: _(msg`Signing links have been generated for this document.`),
duration: 5000,
});
} else {
- router.push(`${documentRootPath}/${document.id}`);
+ await navigate(`${documentRootPath}/${document.id}`);
}
} catch (err) {
console.error(err);
diff --git a/apps/web/src/components/document/document-history-sheet-changes.tsx b/apps/remix/app/components/general/document/document-history-sheet-changes.tsx
similarity index 97%
rename from apps/web/src/components/document/document-history-sheet-changes.tsx
rename to apps/remix/app/components/general/document/document-history-sheet-changes.tsx
index ef3985a61..577dbc473 100644
--- a/apps/web/src/components/document/document-history-sheet-changes.tsx
+++ b/apps/remix/app/components/general/document/document-history-sheet-changes.tsx
@@ -1,5 +1,3 @@
-'use client';
-
import React from 'react';
import { Badge } from '@documenso/ui/primitives/badge';
diff --git a/apps/web/src/components/document/document-history-sheet.tsx b/apps/remix/app/components/general/document/document-history-sheet.tsx
similarity index 99%
rename from apps/web/src/components/document/document-history-sheet.tsx
rename to apps/remix/app/components/general/document/document-history-sheet.tsx
index 8bda3a424..557310ce0 100644
--- a/apps/web/src/components/document/document-history-sheet.tsx
+++ b/apps/remix/app/components/general/document/document-history-sheet.tsx
@@ -1,9 +1,7 @@
-'use client';
-
import { useMemo, useState } from 'react';
-import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { ArrowRightIcon, Loader } from 'lucide-react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx b/apps/remix/app/components/general/document/document-page-view-button.tsx
similarity index 84%
rename from apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx
rename to apps/remix/app/components/general/document/document-page-view-button.tsx
index a477d75c6..e5b76c545 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx
+++ b/apps/remix/app/components/general/document/document-page-view-button.tsx
@@ -1,17 +1,15 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { Document, Recipient, Team, User } from '@prisma/client';
+import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
-import { useSession } from 'next-auth/react';
+import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
+import { useSession } from '@documenso/lib/client-only/providers/session';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
-import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -22,19 +20,15 @@ export type DocumentPageViewButtonProps = {
recipients: Recipient[];
team: Pick | null;
};
- team?: Pick;
};
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
- const { data: session } = useSession();
+ const { user } = useSession();
+
const { toast } = useToast();
const { _ } = useLingui();
- if (!session) {
- return null;
- }
-
- const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
+ const recipient = document.recipients.find((recipient) => recipient.email === user.email);
const isRecipient = !!recipient;
const isPending = document.status === DocumentStatus.PENDING;
@@ -81,7 +75,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
})
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
-
+
{match(role)
.with(RecipientRole.SIGNER, () => (
<>
@@ -106,7 +100,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
))
.with({ isComplete: false }, () => (
-
+
Edit
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-dropdown.tsx b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx
similarity index 79%
rename from apps/web/src/app/(dashboard)/documents/[id]/document-page-view-dropdown.tsx
rename to apps/remix/app/components/general/document/document-page-view-dropdown.tsx
index 5075f342c..390575630 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-dropdown.tsx
+++ b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx
@@ -1,11 +1,10 @@
-'use client';
-
import { useState } from 'react';
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { DocumentStatus } from '@prisma/client';
+import type { Document, Recipient, Team, User } from '@prisma/client';
import {
Copy,
Download,
@@ -16,12 +15,12 @@ import {
Share,
Trash2,
} from 'lucide-react';
-import { useSession } from 'next-auth/react';
+import { Link } from 'react-router';
+import { useNavigate } from 'react-router';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
+import { useSession } from '@documenso/lib/client-only/providers/session';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import { DocumentStatus } from '@documenso/prisma/client';
-import type { Document, Recipient, Team, TeamEmail, User } from '@documenso/prisma/client';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import {
@@ -33,11 +32,11 @@ import {
} from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
-
-import { ResendDocumentActionItem } from '../_action-items/resend-document';
-import { DeleteDocumentDialog } from '../delete-document-dialog';
-import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
+import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
+import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
+import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog';
+import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
+import { useOptionalCurrentTeam } from '~/providers/team';
export type DocumentPageViewDropdownProps = {
document: Document & {
@@ -45,24 +44,22 @@ export type DocumentPageViewDropdownProps = {
recipients: Recipient[];
team: Pick | null;
};
- team?: Pick & { teamEmail: TeamEmail | null };
};
-export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDropdownProps) => {
- const { data: session } = useSession();
+export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownProps) => {
+ const { user } = useSession();
const { toast } = useToast();
const { _ } = useLingui();
+ const navigate = useNavigate();
+ const team = useOptionalCurrentTeam();
+
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
- if (!session) {
- return null;
- }
+ const recipient = document.recipients.find((recipient) => recipient.email === user.email);
- const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
-
- const isOwner = document.user.id === session.user.id;
+ const isOwner = document.user.id === user.id;
const isDraft = document.status === DocumentStatus.DRAFT;
const isPending = document.status === DocumentStatus.PENDING;
const isDeleted = document.deletedAt !== null;
@@ -116,7 +113,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
{(isOwner || isCurrentTeamDocument) && !isComplete && (
-
+
Edit
@@ -131,7 +128,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
)}
-
+
Audit Log
@@ -169,11 +166,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
/>
)}
-
+
- {
+ void navigate(documentsPath);
+ }}
/>
{isDuplicateDialogOpen && (
-
)}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx b/apps/remix/app/components/general/document/document-page-view-information.tsx
similarity index 92%
rename from apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx
rename to apps/remix/app/components/general/document/document-page-view-information.tsx
index ebb6482d5..6ca4d784c 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx
+++ b/apps/remix/app/components/general/document/document-page-view-information.tsx
@@ -1,13 +1,12 @@
-'use client';
-
import { useMemo } from 'react';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { Document, Recipient, User } from '@prisma/client';
import { DateTime } from 'luxon';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
-import type { Document, Recipient, User } from '@documenso/prisma/client';
export type DocumentPageViewInformationProps = {
userId: number;
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recent-activity.tsx b/apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
similarity index 98%
rename from apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recent-activity.tsx
rename to apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
index c6e0787bb..10beae93b 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recent-activity.tsx
+++ b/apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
@@ -1,9 +1,8 @@
-'use client';
-
import { useMemo } from 'react';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { AlertTriangle, CheckCheckIcon, CheckIcon, Loader, MailOpen } from 'lucide-react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recipients.tsx b/apps/remix/app/components/general/document/document-page-view-recipients.tsx
similarity index 94%
rename from apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recipients.tsx
rename to apps/remix/app/components/general/document/document-page-view-recipients.tsx
index ea8ccee15..e74cb6e10 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recipients.tsx
+++ b/apps/remix/app/components/general/document/document-page-view-recipients.tsx
@@ -1,9 +1,8 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
+import type { Document, Recipient } from '@prisma/client';
import {
AlertTriangle,
CheckIcon,
@@ -13,12 +12,11 @@ import {
PenIcon,
PlusIcon,
} from 'lucide-react';
+import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
-import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
-import type { Document, Recipient } from '@documenso/prisma/client';
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
@@ -51,7 +49,7 @@ export const DocumentPageViewRecipients = ({
{document.status !== DocumentStatus.COMPLETED && (
diff --git a/apps/web/src/components/document/document-read-only-fields.tsx b/apps/remix/app/components/general/document/document-read-only-fields.tsx
similarity index 97%
rename from apps/web/src/components/document/document-read-only-fields.tsx
rename to apps/remix/app/components/general/document/document-read-only-fields.tsx
index 926ddaa9d..6970a88be 100644
--- a/apps/web/src/components/document/document-read-only-fields.tsx
+++ b/apps/remix/app/components/general/document/document-read-only-fields.tsx
@@ -1,9 +1,9 @@
-'use client';
-
import { useState } from 'react';
-import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { DocumentMeta } from '@prisma/client';
+import { FieldType, SigningStatus } from '@prisma/client';
import { Clock, EyeOffIcon } from 'lucide-react';
import { P, match } from 'ts-pattern';
@@ -16,8 +16,6 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'
import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import type { DocumentMeta } from '@documenso/prisma/client';
-import { FieldType, SigningStatus } from '@documenso/prisma/client';
import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { cn } from '@documenso/ui/lib/utils';
diff --git a/apps/web/src/components/document/document-recipient-link-copy-dialog.tsx b/apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
similarity index 94%
rename from apps/web/src/components/document/document-recipient-link-copy-dialog.tsx
rename to apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
index bec368f4c..76ba4f56b 100644
--- a/apps/web/src/components/document/document-recipient-link-copy-dialog.tsx
+++ b/apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -1,19 +1,17 @@
-'use client';
-
import { useEffect, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { Recipient } from '@prisma/client';
+import { RecipientRole } from '@prisma/client';
+import { useSearchParams } from 'react-router';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
-import type { Recipient } from '@documenso/prisma/client';
-import { RecipientRole } from '@documenso/prisma/client';
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
@@ -43,7 +41,7 @@ export const DocumentRecipientLinkCopyDialog = ({
const [, copy] = useCopyToClipboard();
- const searchParams = useSearchParams();
+ const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const [open, setOpen] = useState(false);
diff --git a/apps/web/src/components/(dashboard)/document-search/document-search.tsx b/apps/remix/app/components/general/document/document-search.tsx
similarity index 74%
rename from apps/web/src/components/(dashboard)/document-search/document-search.tsx
rename to apps/remix/app/components/general/document/document-search.tsx
index 966452152..dac2ad542 100644
--- a/apps/web/src/components/(dashboard)/document-search/document-search.tsx
+++ b/apps/remix/app/components/general/document/document-search.tsx
@@ -1,11 +1,8 @@
-'use client';
-
import { useCallback, useEffect, useState } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { useSearchParams } from 'react-router';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { Input } from '@documenso/ui/primitives/input';
@@ -13,8 +10,7 @@ import { Input } from '@documenso/ui/primitives/input';
export const DocumentSearch = ({ initialValue = '' }: { initialValue?: string }) => {
const { _ } = useLingui();
- const router = useRouter();
- const searchParams = useSearchParams();
+ const [searchParams, setSearchParams] = useSearchParams();
const [searchTerm, setSearchTerm] = useState(initialValue);
const debouncedSearchTerm = useDebouncedValue(searchTerm, 500);
@@ -23,13 +19,14 @@ export const DocumentSearch = ({ initialValue = '' }: { initialValue?: string })
(term: string) => {
const params = new URLSearchParams(searchParams?.toString() ?? '');
if (term) {
- params.set('search', term);
+ params.set('query', term);
} else {
- params.delete('search');
+ params.delete('query');
}
- router.push(`?${params.toString()}`);
+
+ setSearchParams(params);
},
- [router, searchParams],
+ [searchParams],
);
useEffect(() => {
diff --git a/apps/web/src/components/formatter/document-status.tsx b/apps/remix/app/components/general/document/document-status.tsx
similarity index 97%
rename from apps/web/src/components/formatter/document-status.tsx
rename to apps/remix/app/components/general/document/document-status.tsx
index 494a9b627..fd43a3303 100644
--- a/apps/web/src/components/formatter/document-status.tsx
+++ b/apps/remix/app/components/general/document/document-status.tsx
@@ -1,7 +1,7 @@
import type { HTMLAttributes } from 'react';
import type { MessageDescriptor } from '@lingui/core';
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { CheckCircle2, Clock, File } from 'lucide-react';
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/remix/app/components/general/document/document-upload.tsx
similarity index 81%
rename from apps/web/src/app/(dashboard)/documents/upload-document.tsx
rename to apps/remix/app/components/general/document/document-upload.tsx
index c8fc800a0..d54c05c8c 100644
--- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx
+++ b/apps/remix/app/components/general/document/document-upload.tsx
@@ -1,21 +1,18 @@
-'use client';
-
import { useMemo, useState } from 'react';
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
-import { useSession } from 'next-auth/react';
+import { useNavigate } from 'react-router';
import { match } from 'ts-pattern';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
+import { useSession } from '@documenso/lib/client-only/providers/session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
@@ -23,26 +20,26 @@ import { cn } from '@documenso/ui/lib/utils';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { useToast } from '@documenso/ui/primitives/use-toast';
-export type UploadDocumentProps = {
+import { useOptionalCurrentTeam } from '~/providers/team';
+
+export type DocumentUploadDropzoneProps = {
className?: string;
- team?: {
- id: number;
- url: string;
- };
};
-export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
- const router = useRouter();
+export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProps) => {
+ const { _ } = useLingui();
+ const { toast } = useToast();
+ const { user } = useSession();
+
+ const team = useOptionalCurrentTeam();
+
+ const navigate = useNavigate();
const analytics = useAnalytics();
+
const userTimezone =
TIME_ZONES.find((timezone) => timezone === Intl.DateTimeFormat().resolvedOptions().timeZone) ??
DEFAULT_DOCUMENT_TIME_ZONE;
- const { data: session } = useSession();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
const { quota, remaining, refreshLimits } = useLimits();
const [isLoading, setIsLoading] = useState(false);
@@ -56,26 +53,21 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
: msg`You have reached your document limit.`;
}
- if (!session?.user.emailVerified) {
+ if (!user.emailVerified) {
return msg`Verify your email to upload documents.`;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [remaining.documents, session?.user.emailVerified, team]);
+ }, [remaining.documents, user.emailVerified, team]);
const onFileDrop = async (file: File) => {
try {
setIsLoading(true);
- const { type, data } = await putPdfFile(file);
-
- const { id: documentDataId } = await createDocumentData({
- type,
- data,
- });
+ const response = await putPdfFile(file);
const { id } = await createDocument({
title: file.name,
- documentDataId,
+ documentDataId: response.id,
timezone: userTimezone,
});
@@ -88,12 +80,12 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
});
analytics.capture('App: Document Uploaded', {
- userId: session?.user.id,
+ userId: user.id,
documentId: id,
timestamp: new Date().toISOString(),
});
- router.push(`${formatDocumentsPath(team?.url)}/${id}/edit`);
+ void navigate(`${formatDocumentsPath(team?.url)}/${id}/edit`);
} catch (err) {
const error = AppError.parseError(err);
@@ -131,7 +123,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
;
+
+export type GenericErrorLayoutProps = {
+ children?: React.ReactNode;
+ errorCode?: number;
+ errorCodeMap?: ErrorCodeMap;
+ /**
+ * The primary button to display. If left as undefined, the default /documents link will be displayed.
+ *
+ * Set to null if you want no button.
+ */
+ primaryButton?: React.ReactNode | null;
+
+ /**
+ * The secondary button to display. If left as undefined, the default back button will be displayed.
+ *
+ * Set to null if you want no button.
+ */
+ secondaryButton?: React.ReactNode | null;
+};
+
+export const defaultErrorCodeMap: ErrorCodeMap = {
+ 404: {
+ subHeading: msg`404 Page not found`,
+ heading: msg`Oops! Something went wrong.`,
+ message: msg`The page you are looking for was moved, removed, renamed or might never have existed.`,
+ },
+ 500: {
+ subHeading: msg`500 Internal Server Error`,
+ heading: msg`Oops! Something went wrong.`,
+ message: msg`An unexpected error occurred.`,
+ },
+};
+
+export const GenericErrorLayout = ({
+ children,
+ errorCode,
+ errorCodeMap = defaultErrorCodeMap,
+ primaryButton,
+ secondaryButton,
+}: GenericErrorLayoutProps) => {
+ const navigate = useNavigate();
+ const { _ } = useLingui();
+
+ const team = useOptionalCurrentTeam();
+
+ const { subHeading, heading, message } =
+ errorCodeMap[errorCode || 404] ?? defaultErrorCodeMap[500];
+
+ return (
+
+
+
+
+
+
+
+
+
+
{_(subHeading)}
+
+
{_(heading)}
+
+
{_(message)}
+
+
+ {secondaryButton ||
+ (secondaryButton !== null && (
+ {
+ void navigate(-1);
+ }}
+ >
+
+ Go Back
+
+ ))}
+
+ {primaryButton ||
+ (primaryButton !== null && (
+
+
+ Documents
+
+
+ ))}
+
+ {children}
+
+
+
+
+ );
+};
diff --git a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx b/apps/remix/app/components/general/menu-switcher.tsx
similarity index 85%
rename from apps/web/src/components/(dashboard)/layout/menu-switcher.tsx
rename to apps/remix/app/components/general/menu-switcher.tsx
index 6731845fc..f70a4321a 100644
--- a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx
+++ b/apps/remix/app/components/general/menu-switcher.tsx
@@ -1,23 +1,20 @@
-'use client';
-
import { useState } from 'react';
-import Link from 'next/link';
-import { usePathname } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { CheckCircle2, ChevronsUpDown, Plus, Settings2 } from 'lucide-react';
-import { signOut } from 'next-auth/react';
+import { Link, useLocation } from 'react-router';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import { authClient } from '@documenso/auth/client';
+import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { TEAM_MEMBER_ROLE_MAP, TEAM_URL_REGEX } from '@documenso/lib/constants/teams';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
+import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
-import type { User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
import { cn } from '@documenso/ui/lib/utils';
@@ -35,14 +32,14 @@ import {
const MotionLink = motion(Link);
export type MenuSwitcherProps = {
- user: User;
+ user: SessionUser;
teams: TGetTeamsResponse;
};
export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProps) => {
const { _ } = useLingui();
- const pathname = usePathname();
+ const { pathname } = useLocation();
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
@@ -89,12 +86,12 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
* seemlessly between teams and personal accounts.
*/
const formatRedirectUrlOnSwitch = (teamUrl?: string) => {
- const baseUrl = teamUrl ? `/t/${teamUrl}/` : '/';
+ const baseUrl = teamUrl ? `/t/${teamUrl}` : '';
const currentPathname = (pathname ?? '/').replace(TEAM_URL_REGEX, '');
if (currentPathname === '/templates') {
- return `${baseUrl}templates`;
+ return `${baseUrl}/templates`;
}
return baseUrl;
@@ -109,9 +106,9 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
className="relative flex h-12 flex-row items-center px-0 py-2 ring-0 focus:outline-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-transparent md:px-2"
>
-
+
-
+
@@ -183,7 +176,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
className="text-muted-foreground flex h-5 w-5 items-center justify-center p-0"
asChild
>
-
+
@@ -199,14 +192,10 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
initial="initial"
animate="initial"
whileHover="animate"
- href={formatRedirectUrlOnSwitch(team.url)}
+ to={formatRedirectUrlOnSwitch(team.url)}
>
Create team
@@ -258,14 +247,14 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
{isUserAdmin && (
-
+
Admin panel
)}
-
+
User settings
@@ -273,7 +262,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
{selectedTeam &&
canExecuteTeamAction('MANAGE_TEAM', selectedTeam.currentTeamMember.role) && (
-
+
Team settings
@@ -288,11 +277,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
- signOut({
- callbackUrl: '/',
- })
- }
+ onSelect={async () => authClient.signOut()}
>
Sign Out
diff --git a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx b/apps/remix/app/components/general/metric-card.tsx
similarity index 100%
rename from apps/web/src/components/(dashboard)/metric-card/metric-card.tsx
rename to apps/remix/app/components/general/metric-card.tsx
diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/multiselect-role-combobox.tsx b/apps/remix/app/components/general/multiselect-role-combobox.tsx
similarity index 96%
rename from apps/web/src/app/(dashboard)/admin/users/[id]/multiselect-role-combobox.tsx
rename to apps/remix/app/components/general/multiselect-role-combobox.tsx
index bf7f85d72..f69a9c616 100644
--- a/apps/web/src/app/(dashboard)/admin/users/[id]/multiselect-role-combobox.tsx
+++ b/apps/remix/app/components/general/multiselect-role-combobox.tsx
@@ -1,9 +1,9 @@
import * as React from 'react';
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
+import { Role } from '@prisma/client';
import { Check, ChevronsUpDown } from 'lucide-react';
-import { Role } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
diff --git a/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx b/apps/remix/app/components/general/period-selector.tsx
similarity index 67%
rename from apps/web/src/components/(dashboard)/period-selector/period-selector.tsx
rename to apps/remix/app/components/general/period-selector.tsx
index 94285c138..025925a92 100644
--- a/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx
+++ b/apps/remix/app/components/general/period-selector.tsx
@@ -1,11 +1,9 @@
-'use client';
-
import { useMemo } from 'react';
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
+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,
@@ -14,13 +12,16 @@ import {
SelectValue,
} from '@documenso/ui/primitives/select';
-import { isPeriodSelectorValue } from './types';
+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 = usePathname();
- const searchParams = useSearchParams();
+ const { pathname } = useLocation();
+ const [searchParams] = useSearchParams();
- const router = useRouter();
+ const navigate = useNavigate();
const period = useMemo(() => {
const p = searchParams?.get('period') ?? 'all';
@@ -41,7 +42,7 @@ export const PeriodSelector = () => {
params.delete('period');
}
- router.push(`${pathname}?${params.toString()}`, { scroll: false });
+ void navigate(`${pathname}?${params.toString()}`, { preventScrollReset: true });
};
return (
diff --git a/apps/web/src/components/(dashboard)/refresh-on-focus/refresh-on-focus.tsx b/apps/remix/app/components/general/refresh-on-focus.tsx
similarity index 69%
rename from apps/web/src/components/(dashboard)/refresh-on-focus/refresh-on-focus.tsx
rename to apps/remix/app/components/general/refresh-on-focus.tsx
index 1b2f529b8..775b722f3 100644
--- a/apps/web/src/components/(dashboard)/refresh-on-focus/refresh-on-focus.tsx
+++ b/apps/remix/app/components/general/refresh-on-focus.tsx
@@ -1,15 +1,13 @@
-'use client';
-
import { useCallback, useEffect } from 'react';
-import { useRouter } from 'next/navigation';
+import { useRevalidator } from 'react-router';
export const RefreshOnFocus = () => {
- const { refresh } = useRouter();
+ const { revalidate } = useRevalidator();
const onFocus = useCallback(() => {
- refresh();
- }, [refresh]);
+ void revalidate();
+ }, [revalidate]);
useEffect(() => {
window.addEventListener('focus', onFocus);
diff --git a/apps/web/src/components/(dashboard)/settings/layout/header.tsx b/apps/remix/app/components/general/settings-header.tsx
similarity index 100%
rename from apps/web/src/components/(dashboard)/settings/layout/header.tsx
rename to apps/remix/app/components/general/settings-header.tsx
diff --git a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx b/apps/remix/app/components/general/settings-nav-desktop.tsx
similarity index 65%
rename from apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
rename to apps/remix/app/components/general/settings-nav-desktop.tsx
index 43b1ef988..704637ff3 100644
--- a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
+++ b/apps/remix/app/components/general/settings-nav-desktop.tsx
@@ -1,30 +1,24 @@
-'use client';
-
import type { HTMLAttributes } from 'react';
-import Link from 'next/link';
-import { usePathname } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
import { Braces, CreditCard, Globe2Icon, Lock, User, Users, Webhook } from 'lucide-react';
+import { useLocation } from 'react-router';
+import { Link } from 'react-router';
-import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
+import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
-export type DesktopNavProps = HTMLAttributes;
+export type SettingsDesktopNavProps = HTMLAttributes;
-export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
- const pathname = usePathname();
+export const SettingsDesktopNav = ({ className, ...props }: SettingsDesktopNavProps) => {
+ const { pathname } = useLocation();
- const { getFlag } = useFeatureFlags();
-
- const isBillingEnabled = getFlag('app_billing');
- const isPublicProfileEnabled = getFlag('app_public_profile');
+ const isBillingEnabled = IS_BILLING_ENABLED();
return (
-
+
{
- {isPublicProfileEnabled && (
-
-
-
- Public Profile
-
-
- )}
+
+
+
+ Public Profile
+
+
-
+
{
-
+
{
-
+
{
-
+
{
{isBillingEnabled && (
-
+
;
+export type SettingsMobileNavProps = HTMLAttributes;
-export const MobileNav = ({ className, ...props }: MobileNavProps) => {
- const pathname = usePathname();
+export const SettingsMobileNav = ({ className, ...props }: SettingsMobileNavProps) => {
+ const { pathname } = useLocation();
- const { getFlag } = useFeatureFlags();
-
- const isBillingEnabled = getFlag('app_billing');
- const isPublicProfileEnabled = getFlag('app_public_profile');
+ const isBillingEnabled = IS_BILLING_ENABLED();
return (
-
+
{
- {isPublicProfileEnabled && (
-
-
-
- Public Profile
-
-
- )}
+
+
+
+ Public Profile
+
+
-
+
{
-
+
{
-
+
{
-
+
{
{isBillingEnabled && (
-
+
>;
};
-export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
+export const TeamEmailDropdown = ({ team }: TeamEmailDropdownProps) => {
const { _ } = useLingui();
const { toast } = useToast();
@@ -69,7 +68,7 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
)}
{team.teamEmail && (
- e.preventDefault()}>
@@ -80,7 +79,7 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
/>
)}
- {
+}: TeamLayoutBillingBannerProps) => {
const { _ } = useLingui();
const { toast } = useToast();
diff --git a/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx b/apps/remix/app/components/general/teams/team-settings-nav-desktop.tsx
similarity index 70%
rename from apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx
rename to apps/remix/app/components/general/teams/team-settings-nav-desktop.tsx
index beb31d9b1..e3d01bbaa 100644
--- a/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx
+++ b/apps/remix/app/components/general/teams/team-settings-nav-desktop.tsx
@@ -1,28 +1,19 @@
-'use client';
-
import type { HTMLAttributes } from 'react';
-import Link from 'next/link';
-import { useParams, usePathname } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
import { Braces, CreditCard, Globe2Icon, Settings, Settings2, Users, Webhook } from 'lucide-react';
+import { Link, useLocation, useParams } from 'react-router';
-import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
-export type DesktopNavProps = HTMLAttributes;
+export type TeamSettingsNavDesktopProps = HTMLAttributes;
-export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
- const pathname = usePathname();
+export const TeamSettingsNavDesktop = ({ className, ...props }: TeamSettingsNavDesktopProps) => {
+ const { pathname } = useLocation();
const params = useParams();
- const { getFlag } = useFeatureFlags();
-
- const isPublicProfileEnabled = getFlag('app_public_profile');
-
const teamUrl = typeof params?.teamUrl === 'string' ? params?.teamUrl : '';
const settingsPath = `/t/${teamUrl}/settings`;
@@ -35,7 +26,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
return (
-
+
{
-
+
{
- {isPublicProfileEnabled && (
-
-
-
- Public Profile
-
-
- )}
+
+
+
+ Public Profile
+
+
-
+
{
-
+
{
-
+
{
{IS_BILLING_ENABLED() && (
-
+
;
+export type TeamSettingsNavMobileProps = HTMLAttributes;
-export const MobileNav = ({ className, ...props }: MobileNavProps) => {
- const pathname = usePathname();
+export const TeamSettingsNavMobile = ({ className, ...props }: TeamSettingsNavMobileProps) => {
+ const { pathname } = useLocation();
const params = useParams();
- const { getFlag } = useFeatureFlags();
-
- const isPublicProfileEnabled = getFlag('app_public_profile');
-
const teamUrl = typeof params?.teamUrl === 'string' ? params?.teamUrl : '';
const settingsPath = `/t/${teamUrl}/settings`;
@@ -38,7 +29,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
className={cn('flex flex-wrap items-center justify-start gap-x-2 gap-y-4', className)}
{...props}
>
-
+
{
-
+
{
- {isPublicProfileEnabled && (
-
-
-
- Public Profile
-
-
- )}
+
+
+
+ Public Profile
+
+
-
+
{
-
+
{
-
+
{
{IS_BILLING_ENABLED() && (
-
+
{
- const router = useRouter();
-
const { _ } = useLingui();
const { toast } = useToast();
+ const { revalidate } = useRevalidator();
const isExpired = transferVerification && isTokenExpired(transferVerification.expiresAt);
const { mutateAsync: deleteTeamTransferRequest, isPending } =
trpc.team.deleteTeamTransferRequest.useMutation({
- onSuccess: () => {
+ onSuccess: async () => {
if (!isExpired) {
toast({
title: _(msg`Success`),
@@ -47,7 +44,7 @@ export const TeamTransferStatus = ({
});
}
- router.refresh();
+ await revalidate();
},
onError: () => {
toast({
diff --git a/apps/web/src/app/(dashboard)/templates/template-direct-link-badge.tsx b/apps/remix/app/components/general/template/template-direct-link-badge.tsx
similarity index 94%
rename from apps/web/src/app/(dashboard)/templates/template-direct-link-badge.tsx
rename to apps/remix/app/components/general/template/template-direct-link-badge.tsx
index 66dee8643..9552cb93a 100644
--- a/apps/web/src/app/(dashboard)/templates/template-direct-link-badge.tsx
+++ b/apps/remix/app/components/general/template/template-direct-link-badge.tsx
@@ -1,7 +1,6 @@
-'use client';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { Link2Icon } from 'lucide-react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/edit/edit-template.tsx b/apps/remix/app/components/general/template/template-edit-form.tsx
similarity index 94%
rename from apps/web/src/app/(dashboard)/templates/[id]/edit/edit-template.tsx
rename to apps/remix/app/components/general/template/template-edit-form.tsx
index fc0439c00..972114435 100644
--- a/apps/web/src/app/(dashboard)/templates/[id]/edit/edit-template.tsx
+++ b/apps/remix/app/components/general/template/template-edit-form.tsx
@@ -1,11 +1,8 @@
-'use client';
-
import { useEffect, useState } from 'react';
-import { useRouter } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { useNavigate } from 'react-router';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import {
@@ -30,7 +27,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
-export type EditTemplateFormProps = {
+export type TemplateEditFormProps = {
className?: string;
initialTemplate: TTemplate;
isEnterprise: boolean;
@@ -40,17 +37,16 @@ export type EditTemplateFormProps = {
type EditTemplateStep = 'settings' | 'signers' | 'fields';
const EditTemplateSteps: EditTemplateStep[] = ['settings', 'signers', 'fields'];
-export const EditTemplateForm = ({
+export const TemplateEditForm = ({
initialTemplate,
className,
isEnterprise,
templateRootPath,
-}: EditTemplateFormProps) => {
+}: TemplateEditFormProps) => {
const { _ } = useLingui();
const { toast } = useToast();
- const router = useRouter();
-
+ const navigate = useNavigate();
const team = useOptionalCurrentTeam();
const [step, setStep] = useState('settings');
@@ -144,9 +140,6 @@ export const EditTemplateForm = ({
},
});
- // Router refresh is here to clear the router cache for when navigating to /documents.
- router.refresh();
-
setStep('signers');
} catch (err) {
console.error(err);
@@ -177,9 +170,6 @@ export const EditTemplateForm = ({
}),
]);
- // Router refresh is here to clear the router cache for when navigating to /documents.
- router.refresh();
-
setStep('fields');
} catch (err) {
toast({
@@ -218,7 +208,7 @@ export const EditTemplateForm = ({
duration: 5000,
});
- router.push(templateRootPath);
+ await navigate(templateRootPath);
} catch (err) {
console.error(err);
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx b/apps/remix/app/components/general/template/template-page-view-documents-table.tsx
similarity index 88%
rename from apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx
rename to apps/remix/app/components/general/template/template-page-view-documents-table.tsx
index 4b4b1e57b..b96d83ef2 100644
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx
+++ b/apps/remix/app/components/general/template/template-page-view-documents-table.tsx
@@ -1,20 +1,17 @@
-'use client';
-
import { useMemo } from 'react';
-import { useSearchParams } from 'next/navigation';
-
import type { MessageDescriptor } from '@lingui/core';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { DocumentSource, DocumentStatus as DocumentStatusEnum } from '@prisma/client';
import { InfoIcon } from 'lucide-react';
import { DateTime } from 'luxon';
+import { useSearchParams } from 'react-router';
import { z } from 'zod';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
-import type { Team } from '@documenso/prisma/client';
-import { DocumentSource, DocumentStatus as DocumentStatusEnum } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
@@ -24,15 +21,16 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
-import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
-import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
-import { DocumentStatus } from '~/components/formatter/document-status';
import { SearchParamSelector } from '~/components/forms/search-param-selector';
+import { DocumentSearch } from '~/components/general/document/document-search';
+import { DocumentStatus } from '~/components/general/document/document-status';
+import { StackAvatarsWithTooltip } from '~/components/general/stack-avatars-with-tooltip';
+import { DocumentsTableActionButton } from '~/components/tables/documents-table-action-button';
+import { DocumentsTableActionDropdown } from '~/components/tables/documents-table-action-dropdown';
+import { DataTableTitle } from '~/components/tables/documents-table-title';
+import { useOptionalCurrentTeam } from '~/providers/team';
-import { DataTableActionButton } from '../../documents/data-table-action-button';
-import { DataTableActionDropdown } from '../../documents/data-table-action-dropdown';
-import { DataTableTitle } from '../../documents/data-table-title';
+import { PeriodSelector } from '../period-selector';
const DOCUMENT_SOURCE_LABELS: { [key in DocumentSource]: MessageDescriptor } = {
DOCUMENT: msg`Document`,
@@ -53,18 +51,18 @@ const ZDocumentSearchParamsSchema = ZUrlSearchParamsSchema.extend({
type TemplatePageViewDocumentsTableProps = {
templateId: number;
- team?: Team;
};
export const TemplatePageViewDocumentsTable = ({
templateId,
- team,
}: TemplatePageViewDocumentsTableProps) => {
const { _, i18n } = useLingui();
- const searchParams = useSearchParams();
+ const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
+ const team = useOptionalCurrentTeam();
+
const parsedSearchParams = ZDocumentSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
@@ -175,9 +173,9 @@ export const TemplatePageViewDocumentsTable = ({
header: _(msg`Actions`),
cell: ({ row }) => (
-
+
-
+
),
},
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-information.tsx b/apps/remix/app/components/general/template/template-page-view-information.tsx
similarity index 92%
rename from apps/web/src/app/(dashboard)/templates/[id]/template-page-view-information.tsx
rename to apps/remix/app/components/general/template/template-page-view-information.tsx
index 8f59ff028..7d0d662c5 100644
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-information.tsx
+++ b/apps/remix/app/components/general/template/template-page-view-information.tsx
@@ -1,13 +1,12 @@
-'use client';
-
import { useMemo } from 'react';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { Template, User } from '@prisma/client';
import { DateTime } from 'luxon';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
-import type { Template, User } from '@documenso/prisma/client';
export type TemplatePageViewInformationProps = {
userId: number;
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recent-activity.tsx b/apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
similarity index 96%
rename from apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recent-activity.tsx
rename to apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
index dbdc099ee..e0f3f67c9 100644
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recent-activity.tsx
+++ b/apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
@@ -1,20 +1,16 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
+import { DocumentSource } from '@prisma/client';
import { Loader } from 'lucide-react';
import { DateTime } from 'luxon';
+import { Link } from 'react-router';
import { match } from 'ts-pattern';
-import { DocumentSource } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
export type TemplatePageViewRecentActivityProps = {
templateId: number;
- teamId?: number;
documentRootPath: string;
};
@@ -117,7 +113,7 @@ export const TemplatePageViewRecentActivity = ({
{match(document.source)
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recipients.tsx b/apps/remix/app/components/general/template/template-page-view-recipients.tsx
similarity index 89%
rename from apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recipients.tsx
rename to apps/remix/app/components/general/template/template-page-view-recipients.tsx
index ecbfa9a7f..0a65b3a09 100644
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recipients.tsx
+++ b/apps/remix/app/components/general/template/template-page-view-recipients.tsx
@@ -1,11 +1,11 @@
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { Recipient, Template } from '@prisma/client';
import { PenIcon, PlusIcon } from 'lucide-react';
+import { Link } from 'react-router';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
-import type { Recipient, Template } from '@documenso/prisma/client';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
export type TemplatePageViewRecipientsProps = {
@@ -31,7 +31,7 @@ export const TemplatePageViewRecipients = ({
diff --git a/apps/web/src/components/formatter/template-type.tsx b/apps/remix/app/components/general/template/template-type.tsx
similarity index 91%
rename from apps/web/src/components/formatter/template-type.tsx
rename to apps/remix/app/components/general/template/template-type.tsx
index 03a273470..f1d2ec244 100644
--- a/apps/web/src/components/formatter/template-type.tsx
+++ b/apps/remix/app/components/general/template/template-type.tsx
@@ -1,12 +1,12 @@
import type { HTMLAttributes } from 'react';
import type { MessageDescriptor } from '@lingui/core';
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import type { TemplateType as TemplateTypePrisma } from '@prisma/client';
import { Globe2, Lock } from 'lucide-react';
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
-import type { TemplateType as TemplateTypePrisma } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
type TemplateTypeIcon = {
diff --git a/apps/web/src/components/ui/user-profile-skeleton.tsx b/apps/remix/app/components/general/user-profile-skeleton.tsx
similarity index 96%
rename from apps/web/src/components/ui/user-profile-skeleton.tsx
rename to apps/remix/app/components/general/user-profile-skeleton.tsx
index 8c0fb1906..e4baacd9f 100644
--- a/apps/web/src/components/ui/user-profile-skeleton.tsx
+++ b/apps/remix/app/components/general/user-profile-skeleton.tsx
@@ -1,10 +1,8 @@
-'use client';
-
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
+import type { User } from '@prisma/client';
import { File, User2 } from 'lucide-react';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import type { User } from '@documenso/prisma/client';
import { VerifiedIcon } from '@documenso/ui/icons/verified';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
diff --git a/apps/web/src/components/ui/user-profile-timur.tsx b/apps/remix/app/components/general/user-profile-timur.tsx
similarity index 96%
rename from apps/web/src/components/ui/user-profile-timur.tsx
rename to apps/remix/app/components/general/user-profile-timur.tsx
index ab2500018..f3484bc71 100644
--- a/apps/web/src/components/ui/user-profile-timur.tsx
+++ b/apps/remix/app/components/general/user-profile-timur.tsx
@@ -1,8 +1,4 @@
-'use client';
-
-import Image from 'next/image';
-
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
import { File } from 'lucide-react';
import timurImage from '@documenso/assets/images/timur.png';
@@ -31,7 +27,7 @@ export const UserProfileTimur = ({ className, rows = 2 }: UserProfileTimurProps)
-
{
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
+ const [isPending, setIsPending] = useState(false);
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
- const { mutateAsync: sendConfirmationEmail, isPending } =
- trpc.profile.sendConfirmationEmail.useMutation();
-
const onResendConfirmationEmail = async () => {
+ if (isPending) {
+ return;
+ }
+
+ setIsPending(true);
+
try {
setIsButtonDisabled(true);
-
- await sendConfirmationEmail({ email: email });
+ await authClient.emailPassword.resendVerifyEmail({ email: email });
toast({
title: _(msg`Success`),
@@ -56,6 +58,8 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
variant: 'destructive',
});
}
+
+ setIsPending(false);
};
useEffect(() => {
diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx b/apps/remix/app/components/general/webhook-multiselect-combobox.tsx
similarity index 90%
rename from apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx
rename to apps/remix/app/components/general/webhook-multiselect-combobox.tsx
index 5d5f2f682..d9f7bea62 100644
--- a/apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx
+++ b/apps/remix/app/components/general/webhook-multiselect-combobox.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
-import { Plural, Trans } from '@lingui/macro';
-import { WebhookTriggerEvents } from '@prisma/client/';
+import { Plural, Trans } from '@lingui/react/macro';
+import { WebhookTriggerEvents } from '@prisma/client';
import { Check, ChevronsUpDown } from 'lucide-react';
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
@@ -16,17 +16,17 @@ import {
} from '@documenso/ui/primitives/command';
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
-import { truncateTitle } from '~/helpers/truncate-title';
+import { truncateTitle } from '~/utils/truncate-title';
-type TriggerMultiSelectComboboxProps = {
+type WebhookMultiSelectComboboxProps = {
listValues: string[];
onChange: (_values: string[]) => void;
};
-export const TriggerMultiSelectCombobox = ({
+export const WebhookMultiSelectCombobox = ({
listValues,
onChange,
-}: TriggerMultiSelectComboboxProps) => {
+}: WebhookMultiSelectComboboxProps) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedValues, setSelectedValues] = useState([]);
diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/remix/app/components/tables/admin-dashboard-users-table.tsx
similarity index 92%
rename from apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx
rename to apps/remix/app/components/tables/admin-dashboard-users-table.tsx
index 97a204e91..b5b7737f9 100644
--- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx
+++ b/apps/remix/app/components/tables/admin-dashboard-users-table.tsx
@@ -1,16 +1,13 @@
-'use client';
-
import { useEffect, useMemo, useState, useTransition } from 'react';
-import Link from 'next/link';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import type { Document, Role, Subscription } from '@prisma/client';
import { Edit, Loader } from 'lucide-react';
+import { Link } from 'react-router';
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 { Document, Role, Subscription } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
@@ -33,7 +30,7 @@ type SubscriptionLite = Pick<
type DocumentLite = Pick;
-type UsersDataTableProps = {
+type AdminDashboardUsersTableProps = {
users: UserData[];
totalPages: number;
perPage: number;
@@ -41,13 +38,13 @@ type UsersDataTableProps = {
individualPriceIds: string[];
};
-export const UsersDataTable = ({
+export const AdminDashboardUsersTable = ({
users,
totalPages,
perPage,
page,
individualPriceIds,
-}: UsersDataTableProps) => {
+}: AdminDashboardUsersTableProps) => {
const { _ } = useLingui();
const [isPending, startTransition] = useTransition();
@@ -101,7 +98,7 @@ export const UsersDataTable = ({
cell: ({ row }) => {
return (
-
+
Edit
diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx b/apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
similarity index 92%
rename from apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx
rename to apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
index 8696dab06..58e25b179 100644
--- a/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx
+++ b/apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
@@ -1,20 +1,13 @@
-'use client';
-
import { useMemo } from 'react';
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { type Field, type Recipient, type Signature, SigningStatus } from '@prisma/client';
import { useForm } from 'react-hook-form';
+import { useRevalidator } from 'react-router';
import { z } from 'zod';
-import {
- type Field,
- type Recipient,
- type Signature,
- SigningStatus,
-} from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
@@ -47,11 +40,10 @@ export type RecipientItemProps = {
};
};
-export const RecipientItem = ({ recipient }: RecipientItemProps) => {
+export const AdminDocumentRecipientItemTable = ({ recipient }: RecipientItemProps) => {
const { _ } = useLingui();
const { toast } = useToast();
-
- const router = useRouter();
+ const { revalidate } = useRevalidator();
const form = useForm({
defaultValues: {
@@ -119,7 +111,7 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
description: _(msg`The recipient has been updated successfully`),
});
- router.refresh();
+ await revalidate();
} catch (error) {
toast({
title: _(msg`Failed to update recipient`),
diff --git a/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx b/apps/remix/app/components/tables/admin-leaderboard-table.tsx
similarity index 98%
rename from apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx
rename to apps/remix/app/components/tables/admin-leaderboard-table.tsx
index 596f0051d..44ab8a0a4 100644
--- a/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx
+++ b/apps/remix/app/components/tables/admin-leaderboard-table.tsx
@@ -1,8 +1,6 @@
-'use client';
-
import { useEffect, useMemo, useState, useTransition } from 'react';
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { ChevronDownIcon as CaretSortIcon, Loader } from 'lucide-react';
@@ -30,7 +28,7 @@ type LeaderboardTableProps = {
sortOrder: 'asc' | 'desc';
};
-export const LeaderboardTable = ({
+export const AdminLeaderboardTable = ({
signingVolume,
totalPages,
perPage,
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-data-table.tsx b/apps/remix/app/components/tables/document-logs-table.tsx
similarity index 94%
rename from apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-data-table.tsx
rename to apps/remix/app/components/tables/document-logs-table.tsx
index 45097b594..8cdae26d5 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-data-table.tsx
+++ b/apps/remix/app/components/tables/document-logs-table.tsx
@@ -1,13 +1,10 @@
-'use client';
-
import { useMemo } from 'react';
-import { useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon';
+import { useSearchParams } from 'react-router';
import { UAParser } from 'ua-parser-js';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
@@ -20,7 +17,7 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
-export type DocumentLogsDataTableProps = {
+export type DocumentLogsTableProps = {
documentId: number;
};
@@ -29,10 +26,10 @@ const dateFormat: DateTimeFormatOptions = {
hourCycle: 'h12',
};
-export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
+export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
const { _, i18n } = useLingui();
- const searchParams = useSearchParams();
+ const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx b/apps/remix/app/components/tables/documents-table-action-button.tsx
similarity index 83%
rename from apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx
rename to apps/remix/app/components/tables/documents-table-action-button.tsx
index 1194dfd01..c2fd7cfb4 100644
--- a/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx
+++ b/apps/remix/app/components/tables/documents-table-action-button.tsx
@@ -1,42 +1,39 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { Document, Recipient, Team, User } from '@prisma/client';
+import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react';
-import { useSession } from 'next-auth/react';
+import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
+import { useSession } from '@documenso/lib/client-only/providers/session';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
-import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
-export type DataTableActionButtonProps = {
+import { useOptionalCurrentTeam } from '~/providers/team';
+
+export type DocumentsTableActionButtonProps = {
row: Document & {
user: Pick;
recipients: Recipient[];
team: Pick | null;
};
- team?: Pick;
};
-export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps) => {
- const { data: session } = useSession();
+export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonProps) => {
+ const { user } = useSession();
const { toast } = useToast();
const { _ } = useLingui();
- if (!session) {
- return null;
- }
+ const team = useOptionalCurrentTeam();
- const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
+ const recipient = row.recipients.find((recipient) => recipient.email === user.email);
- const isOwner = row.user.id === session.user.id;
+ const isOwner = row.user.id === user.id;
const isRecipient = !!recipient;
const isDraft = row.status === DocumentStatus.DRAFT;
const isPending = row.status === DocumentStatus.PENDING;
@@ -98,7 +95,7 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
isOwner ? { isDraft: true, isOwner: true } : { isDraft: true, isCurrentTeamDocument: true },
() => (
-
+
Edit
@@ -107,7 +104,7 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
)
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
-
+
{match(role)
.with(RecipientRole.SIGNER, () => (
<>
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/remix/app/components/tables/documents-table-action-dropdown.tsx
similarity index 82%
rename from apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx
rename to apps/remix/app/components/tables/documents-table-action-dropdown.tsx
index 567d8dcd8..e4a2814c8 100644
--- a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx
+++ b/apps/remix/app/components/tables/documents-table-action-dropdown.tsx
@@ -1,11 +1,10 @@
-'use client';
-
import { useState } from 'react';
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { Document, Recipient, Team, User } from '@prisma/client';
+import { DocumentStatus, RecipientRole } from '@prisma/client';
import {
CheckCircle,
Copy,
@@ -19,12 +18,11 @@ import {
Share,
Trash2,
} from 'lucide-react';
-import { useSession } from 'next-auth/react';
+import { Link } from 'react-router';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
+import { useSession } from '@documenso/lib/client-only/providers/session';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
-import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import {
@@ -36,24 +34,25 @@ import {
} from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
+import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
+import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
+import { DocumentMoveDialog } from '~/components/dialogs/document-move-dialog';
+import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog';
+import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
+import { useOptionalCurrentTeam } from '~/providers/team';
-import { ResendDocumentActionItem } from './_action-items/resend-document';
-import { DeleteDocumentDialog } from './delete-document-dialog';
-import { DuplicateDocumentDialog } from './duplicate-document-dialog';
-import { MoveDocumentDialog } from './move-document-dialog';
-
-export type DataTableActionDropdownProps = {
+export type DocumentsTableActionDropdownProps = {
row: Document & {
user: Pick;
recipients: Recipient[];
team: Pick | null;
};
- team?: Pick & { teamEmail?: string };
};
-export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownProps) => {
- const { data: session } = useSession();
+export const DocumentsTableActionDropdown = ({ row }: DocumentsTableActionDropdownProps) => {
+ const { user } = useSession();
+ const team = useOptionalCurrentTeam();
+
const { toast } = useToast();
const { _ } = useLingui();
@@ -61,13 +60,9 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
const [isMoveDialogOpen, setMoveDialogOpen] = useState(false);
- if (!session) {
- return null;
- }
+ const recipient = row.recipients.find((recipient) => recipient.email === user.email);
- const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
-
- const isOwner = row.user.id === session.user.id;
+ const isOwner = row.user.id === user.id;
// const isRecipient = !!recipient;
const isDraft = row.status === DocumentStatus.DRAFT;
const isPending = row.status === DocumentStatus.PENDING;
@@ -119,7 +114,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
{!isDraft && recipient && recipient?.role !== RecipientRole.CC && (
-
+
{recipient?.role === RecipientRole.VIEWER && (
<>
@@ -145,7 +140,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
)}
-
+
Edit
@@ -201,7 +196,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
/>
)}
-
+
-
-
-
);
diff --git a/apps/web/src/app/(dashboard)/documents/empty-state.tsx b/apps/remix/app/components/tables/documents-table-empty-state.tsx
similarity index 88%
rename from apps/web/src/app/(dashboard)/documents/empty-state.tsx
rename to apps/remix/app/components/tables/documents-table-empty-state.tsx
index f97527d0b..e02a1c2bd 100644
--- a/apps/web/src/app/(dashboard)/documents/empty-state.tsx
+++ b/apps/remix/app/components/tables/documents-table-empty-state.tsx
@@ -1,13 +1,13 @@
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Bird, CheckCircle2 } from 'lucide-react';
import { match } from 'ts-pattern';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
-export type EmptyDocumentProps = { status: ExtendedDocumentStatus };
+export type DocumentsTableEmptyStateProps = { status: ExtendedDocumentStatus };
-export const EmptyDocumentState = ({ status }: EmptyDocumentProps) => {
+export const DocumentsTableEmptyState = ({ status }: DocumentsTableEmptyStateProps) => {
const { _ } = useLingui();
const {
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-sender-filter.tsx b/apps/remix/app/components/tables/documents-table-sender-filter.tsx
similarity index 73%
rename from apps/web/src/app/(dashboard)/documents/data-table-sender-filter.tsx
rename to apps/remix/app/components/tables/documents-table-sender-filter.tsx
index 8003c20b8..6251dc980 100644
--- a/apps/web/src/app/(dashboard)/documents/data-table-sender-filter.tsx
+++ b/apps/remix/app/components/tables/documents-table-sender-filter.tsx
@@ -1,25 +1,20 @@
-'use client';
-
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
+import { msg } from '@lingui/core/macro';
+import { Trans } from '@lingui/react/macro';
+import { useLocation, useNavigate, useSearchParams } from 'react-router';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { parseToIntegerArray } from '@documenso/lib/utils/params';
import { trpc } from '@documenso/trpc/react';
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
-type DataTableSenderFilterProps = {
+type DocumentsTableSenderFilterProps = {
teamId: number;
};
-export const DataTableSenderFilter = ({ teamId }: DataTableSenderFilterProps) => {
- const { _ } = useLingui();
-
- const pathname = usePathname();
- const searchParams = useSearchParams();
- const router = useRouter();
+export const DocumentsTableSenderFilter = ({ teamId }: DocumentsTableSenderFilterProps) => {
+ const { pathname } = useLocation();
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
const isMounted = useIsMounted();
@@ -47,7 +42,7 @@ export const DataTableSenderFilter = ({ teamId }: DataTableSenderFilterProps) =>
params.delete('senderIds');
}
- router.push(`${pathname}?${params.toString()}`, { scroll: false });
+ void navigate(`${pathname}?${params.toString()}`, { preventScrollReset: true });
};
return (
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-title.tsx b/apps/remix/app/components/tables/documents-table-title.tsx
similarity index 76%
rename from apps/web/src/app/(dashboard)/documents/data-table-title.tsx
rename to apps/remix/app/components/tables/documents-table-title.tsx
index 39af81195..f31a8dd94 100644
--- a/apps/web/src/app/(dashboard)/documents/data-table-title.tsx
+++ b/apps/remix/app/components/tables/documents-table-title.tsx
@@ -1,12 +1,9 @@
-'use client';
-
-import Link from 'next/link';
-
-import { useSession } from 'next-auth/react';
+import type { Document, Recipient, Team, User } from '@prisma/client';
+import { Link } from 'react-router';
import { match } from 'ts-pattern';
+import { useSession } from '@documenso/lib/client-only/providers/session';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
export type DataTableTitleProps = {
row: Document & {
@@ -18,15 +15,11 @@ export type DataTableTitleProps = {
};
export const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
- const { data: session } = useSession();
+ const { user } = useSession();
- if (!session) {
- return null;
- }
+ const recipient = row.recipients.find((recipient) => recipient.email === user.email);
- const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
-
- const isOwner = row.user.id === session.user.id;
+ const isOwner = row.user.id === user.id;
const isRecipient = !!recipient;
const isCurrentTeamDocument = teamUrl && row.team?.url === teamUrl;
@@ -39,7 +32,7 @@ export const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
})
.with({ isOwner: true }, { isCurrentTeamDocument: true }, () => (
@@ -48,7 +41,7 @@ export const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
))
.with({ isRecipient: true }, () => (
diff --git a/apps/remix/app/components/tables/documents-table.tsx b/apps/remix/app/components/tables/documents-table.tsx
new file mode 100644
index 000000000..f4f5b3cb5
--- /dev/null
+++ b/apps/remix/app/components/tables/documents-table.tsx
@@ -0,0 +1,203 @@
+import { useMemo, useTransition } from 'react';
+
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react';
+import { Loader } from 'lucide-react';
+import { DateTime } from 'luxon';
+import { Link } from 'react-router';
+import { match } from 'ts-pattern';
+
+import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
+import { useSession } from '@documenso/lib/client-only/providers/session';
+import { formatDocumentsPath } from '@documenso/lib/utils/teams';
+import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
+import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
+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 { Skeleton } from '@documenso/ui/primitives/skeleton';
+import { TableCell } from '@documenso/ui/primitives/table';
+
+import { DocumentStatus } from '~/components/general/document/document-status';
+import { useOptionalCurrentTeam } from '~/providers/team';
+
+import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
+import { DocumentsTableActionButton } from './documents-table-action-button';
+import { DocumentsTableActionDropdown } from './documents-table-action-dropdown';
+
+export type DocumentsTableProps = {
+ data?: TFindDocumentsResponse;
+ isLoading?: boolean;
+ isLoadingError?: boolean;
+};
+
+type DocumentsTableRow = TFindDocumentsResponse['data'][number];
+
+export const DocumentsTable = ({ data, isLoading, isLoadingError }: DocumentsTableProps) => {
+ const { _, i18n } = useLingui();
+
+ const team = useOptionalCurrentTeam();
+ const [isPending, startTransition] = useTransition();
+
+ const updateSearchParams = useUpdateSearchParams();
+
+ const columns = useMemo(() => {
+ return [
+ {
+ header: _(msg`Created`),
+ accessorKey: 'createdAt',
+ cell: ({ row }) =>
+ i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
+ },
+ {
+ header: _(msg`Title`),
+ cell: ({ row }) => ,
+ },
+ {
+ id: 'sender',
+ header: _(msg`Sender`),
+ cell: ({ row }) => row.original.user.name ?? row.original.user.email,
+ },
+ {
+ header: _(msg`Recipient`),
+ accessorKey: 'recipient',
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ header: _(msg`Status`),
+ accessorKey: 'status',
+ cell: ({ row }) => ,
+ size: 140,
+ },
+ {
+ header: _(msg`Actions`),
+ cell: ({ row }) =>
+ (!row.original.deletedAt || row.original.status === ExtendedDocumentStatus.COMPLETED) && (
+
+
+
+
+ ),
+ },
+ ] satisfies DataTableColumnDef[];
+ }, [team]);
+
+ const onPaginationChange = (page: number, perPage: number) => {
+ startTransition(() => {
+ updateSearchParams({
+ page,
+ perPage,
+ });
+ });
+ };
+
+ const results = data ?? {
+ data: [],
+ perPage: 10,
+ currentPage: 1,
+ totalPages: 1,
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ),
+ }}
+ >
+ {(table) => }
+
+
+ {isPending && (
+
+
+
+ )}
+
+ );
+};
+
+type DataTableTitleProps = {
+ row: DocumentsTableRow;
+ teamUrl?: string;
+};
+
+const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
+ const { user } = useSession();
+
+ const recipient = row.recipients.find((recipient) => recipient.email === user.email);
+
+ const isOwner = row.user.id === user.id;
+ const isRecipient = !!recipient;
+ const isCurrentTeamDocument = teamUrl && row.team?.url === teamUrl;
+
+ const documentsPath = formatDocumentsPath(isCurrentTeamDocument ? teamUrl : undefined);
+
+ return match({
+ isOwner,
+ isRecipient,
+ isCurrentTeamDocument,
+ })
+ .with({ isOwner: true }, { isCurrentTeamDocument: true }, () => (
+
+ {row.title}
+
+ ))
+ .with({ isRecipient: true }, () => (
+
+ {row.title}
+
+ ))
+ .otherwise(() => (
+
+ {row.title}
+
+ ));
+};
diff --git a/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/audit-log/data-table.tsx b/apps/remix/app/components/tables/internal-audit-log-table.tsx
similarity index 95%
rename from apps/web/src/app/(internal)/%5F%5Fhtmltopdf/audit-log/data-table.tsx
rename to apps/remix/app/components/tables/internal-audit-log-table.tsx
index a3c77c15e..addbb4174 100644
--- a/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/audit-log/data-table.tsx
+++ b/apps/remix/app/components/tables/internal-audit-log-table.tsx
@@ -1,4 +1,4 @@
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon';
@@ -28,7 +28,7 @@ const dateFormat: DateTimeFormatOptions = {
/**
* DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
*/
-export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => {
+export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
const { _ } = useLingui();
const parser = new UAParser();
diff --git a/apps/web/src/app/(dashboard)/settings/public-profile/public-templates-data-table.tsx b/apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
similarity index 95%
rename from apps/web/src/app/(dashboard)/settings/public-profile/public-templates-data-table.tsx
rename to apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
index b8f3034b8..d3c89cdab 100644
--- a/apps/web/src/app/(dashboard)/settings/public-profile/public-templates-data-table.tsx
+++ b/apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
@@ -1,15 +1,14 @@
-'use client';
-
import { useMemo, useState } from 'react';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { TemplateDirectLink } from '@prisma/client';
+import { TemplateType } from '@prisma/client';
import { EditIcon, FileIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
-import type { TemplateDirectLink } from '@documenso/prisma/client';
-import { TemplateType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
import {
@@ -22,13 +21,13 @@ import {
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
+import { ManagePublicTemplateDialog } from '~/components/dialogs/public-profile-template-manage-dialog';
type DirectTemplate = FindTemplateRow & {
directLink: Pick;
};
-export const PublicTemplatesDataTable = () => {
+export const SettingsPublicProfileTemplatesTable = () => {
const { _ } = useLingui();
const { toast } = useToast();
diff --git a/apps/web/src/app/(dashboard)/settings/security/activity/user-security-activity-data-table.tsx b/apps/remix/app/components/tables/settings-security-activity-table.tsx
similarity index 92%
rename from apps/web/src/app/(dashboard)/settings/security/activity/user-security-activity-data-table.tsx
rename to apps/remix/app/components/tables/settings-security-activity-table.tsx
index 660c2346f..9001425c6 100644
--- a/apps/web/src/app/(dashboard)/settings/security/activity/user-security-activity-data-table.tsx
+++ b/apps/remix/app/components/tables/settings-security-activity-table.tsx
@@ -1,13 +1,10 @@
-'use client';
-
import { useMemo } from 'react';
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
+import { useLocation, useNavigate, useSearchParams } from 'react-router';
import { UAParser } from 'ua-parser-js';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
@@ -25,12 +22,12 @@ const dateFormat: DateTimeFormatOptions = {
hourCycle: 'h12',
};
-export const UserSecurityActivityDataTable = () => {
+export const SettingsSecurityActivityTable = () => {
const { _, i18n } = useLingui();
- const pathname = usePathname();
- const router = useRouter();
- const searchParams = useSearchParams();
+ const { pathname } = useLocation();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
@@ -128,7 +125,7 @@ export const UserSecurityActivityDataTable = () => {
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
hasFilters={parsedSearchParams.page !== undefined || parsedSearchParams.perPage !== undefined}
- onClearFilters={() => router.push(pathname ?? '/')}
+ onClearFilters={async () => navigate(pathname ?? '/')}
error={{
enable: isLoadingError,
}}
diff --git a/apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table-actions.tsx b/apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
similarity index 95%
rename from apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table-actions.tsx
rename to apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
index 72142da41..e86800149 100644
--- a/apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table-actions.tsx
+++ b/apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
@@ -1,8 +1,9 @@
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@@ -30,7 +31,7 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
-export type UserPasskeysDataTableActionsProps = {
+export type SettingsSecurityPasskeyTableActionsProps = {
className?: string;
passkeyId: string;
passkeyName: string;
@@ -42,11 +43,11 @@ const ZUpdatePasskeySchema = z.object({
type TUpdatePasskeySchema = z.infer;
-export const UserPasskeysDataTableActions = ({
+export const SettingsSecurityPasskeyTableActions = ({
className,
passkeyId,
passkeyName,
-}: UserPasskeysDataTableActionsProps) => {
+}: SettingsSecurityPasskeyTableActionsProps) => {
const { _ } = useLingui();
const { toast } = useToast();
@@ -67,6 +68,8 @@ export const UserPasskeysDataTableActions = ({
title: _(msg`Success`),
description: _(msg`Passkey has been updated`),
});
+
+ setIsUpdateDialogOpen(false);
},
onError: () => {
toast({
@@ -87,6 +90,8 @@ export const UserPasskeysDataTableActions = ({
title: _(msg`Success`),
description: _(msg`Passkey has been removed`),
});
+
+ setIsDeleteDialogOpen(false);
},
onError: () => {
toast({
diff --git a/apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table.tsx b/apps/remix/app/components/tables/settings-security-passkey-table.tsx
similarity index 87%
rename from apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table.tsx
rename to apps/remix/app/components/tables/settings-security-passkey-table.tsx
index 169630f20..3d202900a 100644
--- a/apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table.tsx
+++ b/apps/remix/app/components/tables/settings-security-passkey-table.tsx
@@ -1,12 +1,9 @@
-'use client';
-
import { useMemo } from 'react';
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
+import { useLocation, useNavigate, useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
@@ -17,14 +14,14 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
-import { UserPasskeysDataTableActions } from './user-passkeys-data-table-actions';
+import { SettingsSecurityPasskeyTableActions } from './settings-security-passkey-table-actions';
-export const UserPasskeysDataTable = () => {
+export const SettingsSecurityPasskeyTable = () => {
const { _ } = useLingui();
- const pathname = usePathname();
- const router = useRouter();
- const searchParams = useSearchParams();
+ const { pathname } = useLocation();
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
@@ -76,7 +73,7 @@ export const UserPasskeysDataTable = () => {
{
id: 'actions',
cell: ({ row }) => (
- {
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
hasFilters={parsedSearchParams.page !== undefined || parsedSearchParams.perPage !== undefined}
- onClearFilters={() => router.push(pathname ?? '/')}
+ onClearFilters={async () => navigate(pathname ?? '/')}
error={{
enable: isLoadingError,
}}
diff --git a/apps/web/src/components/(teams)/tables/team-billing-invoices-data-table.tsx b/apps/remix/app/components/tables/team-settings-billing-invoices-table.tsx
similarity index 91%
rename from apps/web/src/components/(teams)/tables/team-billing-invoices-data-table.tsx
rename to apps/remix/app/components/tables/team-settings-billing-invoices-table.tsx
index 81f5c1c49..14e843c36 100644
--- a/apps/web/src/components/(teams)/tables/team-billing-invoices-data-table.tsx
+++ b/apps/remix/app/components/tables/team-settings-billing-invoices-table.tsx
@@ -1,13 +1,11 @@
-'use client';
-
import { useMemo } from 'react';
-import Link from 'next/link';
-
-import { Plural, Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Plural, Trans } from '@lingui/react/macro';
import { File } from 'lucide-react';
import { DateTime } from 'luxon';
+import { Link } from 'react-router';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
@@ -17,11 +15,13 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
-export type TeamBillingInvoicesDataTableProps = {
+export type TeamSettingsBillingInvoicesTableProps = {
teamId: number;
};
-export const TeamBillingInvoicesDataTable = ({ teamId }: TeamBillingInvoicesDataTableProps) => {
+export const TeamSettingsBillingInvoicesTable = ({
+ teamId,
+}: TeamSettingsBillingInvoicesTableProps) => {
const { _ } = useLingui();
const { data, isLoading, isLoadingError } = trpc.team.findTeamInvoices.useQuery(
@@ -96,7 +96,7 @@ export const TeamBillingInvoicesDataTable = ({ teamId }: TeamBillingInvoicesData
asChild
disabled={typeof row.original.hostedInvoicePdf !== 'string'}
>
-
+
View
@@ -106,7 +106,7 @@ export const TeamBillingInvoicesDataTable = ({ teamId }: TeamBillingInvoicesData
asChild
disabled={typeof row.original.hostedInvoicePdf !== 'string'}
>
-
+
Download
diff --git a/apps/web/src/components/(teams)/tables/team-member-invites-data-table.tsx b/apps/remix/app/components/tables/team-settings-member-invites-table.tsx
similarity index 93%
rename from apps/web/src/components/(teams)/tables/team-member-invites-data-table.tsx
rename to apps/remix/app/components/tables/team-settings-member-invites-table.tsx
index 8a57be81c..fbe1346df 100644
--- a/apps/web/src/components/(teams)/tables/team-member-invites-data-table.tsx
+++ b/apps/remix/app/components/tables/team-settings-member-invites-table.tsx
@@ -1,12 +1,10 @@
-'use client';
-
import { useMemo } from 'react';
-import { useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { History, MoreHorizontal, Trash2 } from 'lucide-react';
+import { useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
@@ -27,13 +25,12 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { useToast } from '@documenso/ui/primitives/use-toast';
-export type TeamMemberInvitesDataTableProps = {
- teamId: number;
-};
+import { useCurrentTeam } from '~/providers/team';
-export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTableProps) => {
- const searchParams = useSearchParams();
+export const TeamSettingsMemberInvitesTable = () => {
+ const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
+ const team = useCurrentTeam();
const { _, i18n } = useLingui();
const { toast } = useToast();
@@ -42,7 +39,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
const { data, isLoading, isLoadingError } = trpc.team.findTeamMemberInvites.useQuery(
{
- teamId,
+ teamId: team.id,
query: parsedSearchParams.query,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
@@ -142,7 +139,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
resendTeamMemberInvitation({
- teamId,
+ teamId: team.id,
invitationId: row.original.id,
})
}
@@ -154,7 +151,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
deleteTeamMemberInvitations({
- teamId,
+ teamId: team.id,
invitationIds: [row.original.id],
})
}
diff --git a/apps/web/src/components/(teams)/tables/team-members-data-table.tsx b/apps/remix/app/components/tables/team-settings-members-table.tsx
similarity index 82%
rename from apps/web/src/components/(teams)/tables/team-members-data-table.tsx
rename to apps/remix/app/components/tables/team-settings-members-table.tsx
index e92efb727..d1d658ecd 100644
--- a/apps/web/src/components/(teams)/tables/team-members-data-table.tsx
+++ b/apps/remix/app/components/tables/team-settings-members-table.tsx
@@ -1,19 +1,16 @@
-'use client';
-
import { useMemo } from 'react';
-import { useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { Edit, MoreHorizontal, Trash2 } from 'lucide-react';
+import { useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
-import type { TeamMemberRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
@@ -29,32 +26,22 @@ import {
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
-import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog';
-import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
+import { TeamMemberDeleteDialog } from '~/components/dialogs/team-member-delete-dialog';
+import { TeamMemberUpdateDialog } from '~/components/dialogs/team-member-update-dialog';
+import { useCurrentTeam } from '~/providers/team';
-export type TeamMembersDataTableProps = {
- currentUserTeamRole: TeamMemberRole;
- teamOwnerUserId: number;
- teamId: number;
- teamName: string;
-};
-
-export const TeamMembersDataTable = ({
- currentUserTeamRole,
- teamOwnerUserId,
- teamId,
- teamName,
-}: TeamMembersDataTableProps) => {
+export const TeamSettingsMembersDataTable = () => {
const { _, i18n } = useLingui();
- const searchParams = useSearchParams();
+ const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
+ const team = useCurrentTeam();
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isLoadingError } = trpc.team.findTeamMembers.useQuery(
{
- teamId,
+ teamId: team.id,
query: parsedSearchParams.query,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
@@ -103,7 +90,7 @@ export const TeamMembersDataTable = ({
header: _(msg`Role`),
accessorKey: 'role',
cell: ({ row }) =>
- teamOwnerUserId === row.original.userId
+ team.ownerUserId === row.original.userId
? _(msg`Owner`)
: _(TEAM_MEMBER_ROLE_MAP[row.original.role]),
},
@@ -125,8 +112,8 @@ export const TeamMembersDataTable = ({
Actions
- e.preventDefault()}
title="Update team member role"
@@ -146,9 +133,9 @@ export const TeamMembersDataTable = ({
}
/>
- e.preventDefault()}
disabled={
- teamOwnerUserId === row.original.userId ||
- !isTeamRoleWithinUserHierarchy(currentUserTeamRole, row.original.role)
+ team.ownerUserId === row.original.userId ||
+ !isTeamRoleWithinUserHierarchy(team.currentTeamMember.role, row.original.role)
}
title={_(msg`Remove team member`)}
>
diff --git a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx b/apps/remix/app/components/tables/templates-table-action-dropdown.tsx
similarity index 71%
rename from apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx
rename to apps/remix/app/components/tables/templates-table-action-dropdown.tsx
index 28fac6118..c2d26e2ea 100644
--- a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx
+++ b/apps/remix/app/components/tables/templates-table-action-dropdown.tsx
@@ -1,14 +1,11 @@
-'use client';
-
import { useState } from 'react';
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
+import type { Recipient, Template, TemplateDirectLink } from '@prisma/client';
import { Copy, Edit, MoreHorizontal, MoveRight, Share2Icon, Trash2 } from 'lucide-react';
-import { useSession } from 'next-auth/react';
+import { Link } from 'react-router';
-import type { Recipient, Template, TemplateDirectLink } from '@documenso/prisma/client';
+import { useSession } from '@documenso/lib/client-only/providers/session';
import {
DropdownMenu,
DropdownMenuContent,
@@ -17,37 +14,43 @@ import {
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
-import { DeleteTemplateDialog } from './delete-template-dialog';
-import { DuplicateTemplateDialog } from './duplicate-template-dialog';
-import { MoveTemplateDialog } from './move-template-dialog';
-import { TemplateDirectLinkDialog } from './template-direct-link-dialog';
+import { TemplateDeleteDialog } from '../dialogs/template-delete-dialog';
+import { TemplateDirectLinkDialog } from '../dialogs/template-direct-link-dialog';
+import { TemplateDuplicateDialog } from '../dialogs/template-duplicate-dialog';
+import { TemplateMoveDialog } from '../dialogs/template-move-dialog';
-export type DataTableActionDropdownProps = {
+export type TemplatesTableActionDropdownProps = {
row: Template & {
directLink?: Pick | null;
recipients: Recipient[];
};
templateRootPath: string;
teamId?: number;
+ onDelete?: () => Promise | void;
+ onMove?: ({
+ templateId,
+ teamUrl,
+ }: {
+ templateId: number;
+ teamUrl: string;
+ }) => Promise | void;
};
-export const DataTableActionDropdown = ({
+export const TemplatesTableActionDropdown = ({
row,
templateRootPath,
teamId,
-}: DataTableActionDropdownProps) => {
- const { data: session } = useSession();
+ onDelete,
+ onMove,
+}: TemplatesTableActionDropdownProps) => {
+ const { user } = useSession();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isTemplateDirectLinkDialogOpen, setTemplateDirectLinkDialogOpen] = useState(false);
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
const [isMoveDialogOpen, setMoveDialogOpen] = useState(false);
- if (!session) {
- return null;
- }
-
- const isOwner = row.userId === session.user.id;
+ const isOwner = row.userId === user.id;
const isTeamTemplate = row.teamId === teamId;
return (
@@ -60,7 +63,7 @@ export const DataTableActionDropdown = ({
Action
-
+
Edit
@@ -95,9 +98,8 @@ export const DataTableActionDropdown = ({
-
@@ -108,17 +110,18 @@ export const DataTableActionDropdown = ({
onOpenChange={setTemplateDirectLinkDialogOpen}
/>
-
-
);
diff --git a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx b/apps/remix/app/components/tables/templates-table.tsx
similarity index 65%
rename from apps/web/src/app/(dashboard)/templates/data-table-templates.tsx
rename to apps/remix/app/components/tables/templates-table.tsx
index d198cdab5..08f46fe40 100644
--- a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx
+++ b/apps/remix/app/components/tables/templates-table.tsx
@@ -1,54 +1,61 @@
-'use client';
-
import { useMemo, useTransition } from 'react';
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { AlertTriangle, Globe2Icon, InfoIcon, Link2Icon, Loader, LockIcon } from 'lucide-react';
+import { Link } from 'react-router';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
+import { formatTemplatesPath } from '@documenso/lib/utils/teams';
+import type { TFindTemplatesResponse } from '@documenso/trpc/server/template-router/schema';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
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 { Skeleton } from '@documenso/ui/primitives/skeleton';
+import { TableCell } from '@documenso/ui/primitives/table';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-import { TemplateType } from '~/components/formatter/template-type';
+import { TemplateType } from '~/components/general/template/template-type';
+import { useOptionalCurrentTeam } from '~/providers/team';
-import { DataTableActionDropdown } from './data-table-action-dropdown';
-import { DataTableTitle } from './data-table-title';
-import { TemplateDirectLinkBadge } from './template-direct-link-badge';
-import { UseTemplateDialog } from './use-template-dialog';
+import { TemplateUseDialog } from '../dialogs/template-use-dialog';
+import { TemplateDirectLinkBadge } from '../general/template/template-direct-link-badge';
+import { TemplatesTableActionDropdown } from './templates-table-action-dropdown';
-type TemplatesDataTableProps = {
- templates: FindTemplateRow[];
- perPage: number;
- page: number;
- totalPages: number;
+type TemplatesTableProps = {
+ data?: TFindTemplatesResponse;
+ isLoading?: boolean;
+ isLoadingError?: boolean;
documentRootPath: string;
templateRootPath: string;
- teamId?: number;
};
-export const TemplatesDataTable = ({
- templates,
- perPage,
- page,
- totalPages,
+type TemplatesTableRow = TFindTemplatesResponse['data'][number];
+
+export const TemplatesTable = ({
+ data,
+ isLoading,
+ isLoadingError,
documentRootPath,
templateRootPath,
- teamId,
-}: TemplatesDataTableProps) => {
+}: TemplatesTableProps) => {
+ const { _, i18n } = useLingui();
+ const { remaining } = useLimits();
+
+ const team = useOptionalCurrentTeam();
+
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
- const { _, i18n } = useLingui();
- const { remaining } = useLimits();
+ const formatTemplateLink = (row: TemplatesTableRow) => {
+ const isCurrentTeamTemplate = team?.url && row.team?.url === team?.url;
+ const path = formatTemplatesPath(isCurrentTeamTemplate ? team?.url : undefined);
+ return `${path}/${row.id}`;
+ };
const columns = useMemo(() => {
return [
@@ -59,7 +66,14 @@ export const TemplatesDataTable = ({
},
{
header: _(msg`Title`),
- cell: ({ row }) => ,
+ cell: ({ row }) => (
+
+ {row.original.title}
+
+ ),
},
{
header: () => (
@@ -102,11 +116,11 @@ export const TemplatesDataTable = ({
- {teamId ? Team Only : Private }
+ {team?.id ? Team Only : Private }
- {teamId ? (
+ {team?.id ? (
Team only templates are not linked anywhere and are visible only to your
team.
@@ -142,7 +156,7 @@ export const TemplatesDataTable = ({
cell: ({ row }) => {
return (
-
-
);
},
},
- ] satisfies DataTableColumnDef<(typeof templates)[number]>[];
- }, [documentRootPath, teamId, templateRootPath]);
+ ] satisfies DataTableColumnDef[];
+ }, [documentRootPath, team?.id, templateRootPath]);
const onPaginationChange = (page: number, perPage: number) => {
startTransition(() => {
@@ -171,6 +185,13 @@ export const TemplatesDataTable = ({
});
};
+ const results = data ?? {
+ data: [],
+ perPage: 10,
+ currentPage: 1,
+ totalPages: 1,
+ };
+
return (
{remaining.documents === 0 && (
@@ -182,7 +203,7 @@ export const TemplatesDataTable = ({
You have reached your document limit.{' '}
-
+
Upgrade your account to continue!
@@ -192,11 +213,39 @@ export const TemplatesDataTable = ({
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ),
+ }}
>
{(table) => }
diff --git a/apps/web/src/components/(teams)/tables/current-user-teams-data-table.tsx b/apps/remix/app/components/tables/user-settings-current-teams-table.tsx
similarity index 86%
rename from apps/web/src/components/(teams)/tables/current-user-teams-data-table.tsx
rename to apps/remix/app/components/tables/user-settings-current-teams-table.tsx
index d9984aace..688a5cb62 100644
--- a/apps/web/src/components/(teams)/tables/current-user-teams-data-table.tsx
+++ b/apps/remix/app/components/tables/user-settings-current-teams-table.tsx
@@ -1,17 +1,16 @@
-'use client';
-
import { useMemo } from 'react';
-import Link from 'next/link';
-import { useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { useSearchParams } from 'react-router';
+import { Link } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { NEXT_PUBLIC_WEBAPP_URL, WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
+import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
@@ -22,12 +21,12 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
-import { LeaveTeamDialog } from '../dialogs/leave-team-dialog';
+import { TeamLeaveDialog } from '~/components/dialogs/team-leave-dialog';
-export const CurrentUserTeamsDataTable = () => {
+export const UserSettingsCurrentTeamsDataTable = () => {
const { _, i18n } = useLingui();
- const searchParams = useSearchParams();
+ const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
@@ -63,15 +62,15 @@ export const CurrentUserTeamsDataTable = () => {
header: _(msg`Team`),
accessorKey: 'name',
cell: ({ row }) => (
-
+
{row.original.name}
}
- secondaryText={`${WEBAPP_BASE_URL}/t/${row.original.url}`}
+ secondaryText={`${NEXT_PUBLIC_WEBAPP_URL()}/t/${row.original.url}`}
/>
),
@@ -95,13 +94,13 @@ export const CurrentUserTeamsDataTable = () => {
{canExecuteTeamAction('MANAGE_TEAM', row.original.currentTeamMember.role) && (
-
+
Manage
)}
-
void;
};
-export const PendingUserTeamsDataTableActions = ({
+export const UserSettingsPendingTeamsTableActions = ({
className,
pendingTeamId,
onPayClick,
-}: PendingUserTeamsDataTableActionsProps) => {
+}: UserSettingsPendingTeamsTableActionsProps) => {
const { _ } = useLingui();
const { toast } = useToast();
diff --git a/apps/web/src/components/(teams)/tables/pending-user-teams-data-table.tsx b/apps/remix/app/components/tables/user-settings-pending-teams-table.tsx
similarity index 87%
rename from apps/web/src/components/(teams)/tables/pending-user-teams-data-table.tsx
rename to apps/remix/app/components/tables/user-settings-pending-teams-table.tsx
index b656308f7..bdd86b4eb 100644
--- a/apps/web/src/components/(teams)/tables/pending-user-teams-data-table.tsx
+++ b/apps/remix/app/components/tables/user-settings-pending-teams-table.tsx
@@ -1,14 +1,11 @@
-'use client';
-
import { useEffect, useMemo, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { trpc } from '@documenso/trpc/react';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
@@ -18,13 +15,14 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
-import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog';
-import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
+import { TeamCheckoutCreateDialog } from '~/components/dialogs/team-checkout-create-dialog';
-export const PendingUserTeamsDataTable = () => {
+import { UserSettingsPendingTeamsTableActions } from './user-settings-pending-teams-table-actions';
+
+export const UserSettingsPendingTeamsDataTable = () => {
const { _, i18n } = useLingui();
- const searchParams = useSearchParams();
+ const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
@@ -68,7 +66,7 @@ export const PendingUserTeamsDataTable = () => {
primaryText={
{row.original.name}
}
- secondaryText={`${WEBAPP_BASE_URL}/t/${row.original.url}`}
+ secondaryText={`${NEXT_PUBLIC_WEBAPP_URL()}/t/${row.original.url}`}
/>
),
},
@@ -80,7 +78,7 @@ export const PendingUserTeamsDataTable = () => {
{
id: 'actions',
cell: ({ row }) => (
- {
{(table) => }
- setCheckoutPendingTeamId(null)}
/>
diff --git a/apps/web/src/components/(teams)/tables/user-settings-teams-page-data-table.tsx b/apps/remix/app/components/tables/user-settings-teams-page-table.tsx
similarity index 71%
rename from apps/web/src/components/(teams)/tables/user-settings-teams-page-data-table.tsx
rename to apps/remix/app/components/tables/user-settings-teams-page-table.tsx
index bac1dbf44..8773c9b73 100644
--- a/apps/web/src/components/(teams)/tables/user-settings-teams-page-data-table.tsx
+++ b/apps/remix/app/components/tables/user-settings-teams-page-table.tsx
@@ -1,27 +1,24 @@
-'use client';
-
import { useEffect, useState } from 'react';
-import Link from 'next/link';
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { Link, useSearchParams } from 'react-router';
+import { useLocation } from 'react-router';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { trpc } from '@documenso/trpc/react';
import { Input } from '@documenso/ui/primitives/input';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
-import { CurrentUserTeamsDataTable } from './current-user-teams-data-table';
-import { PendingUserTeamsDataTable } from './pending-user-teams-data-table';
+import { UserSettingsCurrentTeamsDataTable } from './user-settings-current-teams-table';
+import { UserSettingsPendingTeamsDataTable } from './user-settings-pending-teams-table';
export const UserSettingsTeamsPageDataTable = () => {
const { _ } = useLingui();
- const searchParams = useSearchParams();
- const router = useRouter();
- const pathname = usePathname();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const { pathname } = useLocation();
const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? '');
@@ -40,10 +37,6 @@ export const UserSettingsTeamsPageDataTable = () => {
* Handle debouncing the search query.
*/
useEffect(() => {
- if (!pathname) {
- return;
- }
-
const params = new URLSearchParams(searchParams?.toString());
params.set('query', debouncedSearchQuery);
@@ -52,8 +45,8 @@ export const UserSettingsTeamsPageDataTable = () => {
params.delete('query');
}
- router.push(`${pathname}?${params.toString()}`);
- }, [debouncedSearchQuery, pathname, router, searchParams]);
+ setSearchParams(params);
+ }, [debouncedSearchQuery, pathname, searchParams]);
return (
@@ -67,13 +60,13 @@ export const UserSettingsTeamsPageDataTable = () => {
-
+
Active
-
+
Pending
{data && data.count > 0 && (
{data.count}
@@ -84,7 +77,11 @@ export const UserSettingsTeamsPageDataTable = () => {
- {currentTab === 'pending' ? : }
+ {currentTab === 'pending' ? (
+
+ ) : (
+
+ )}
);
};
diff --git a/apps/remix/app/entry.client.tsx b/apps/remix/app/entry.client.tsx
new file mode 100644
index 000000000..a60ac5063
--- /dev/null
+++ b/apps/remix/app/entry.client.tsx
@@ -0,0 +1,29 @@
+import { StrictMode, startTransition } from 'react';
+
+import { i18n } from '@lingui/core';
+import { detect, fromHtmlTag } from '@lingui/detect-locale';
+import { I18nProvider } from '@lingui/react';
+import { hydrateRoot } from 'react-dom/client';
+import { HydratedRouter } from 'react-router/dom';
+
+import { dynamicActivate } from '@documenso/lib/utils/i18n';
+
+async function main() {
+ const locale = detect(fromHtmlTag('lang')) || 'en';
+
+ await dynamicActivate(locale);
+
+ startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+
+ ,
+ );
+ });
+}
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+main();
diff --git a/apps/remix/app/entry.server.tsx b/apps/remix/app/entry.server.tsx
new file mode 100644
index 000000000..6caeba746
--- /dev/null
+++ b/apps/remix/app/entry.server.tsx
@@ -0,0 +1,82 @@
+import { i18n } from '@lingui/core';
+import { I18nProvider } from '@lingui/react';
+import { createReadableStreamFromReadable } from '@react-router/node';
+import { isbot } from 'isbot';
+import { PassThrough } from 'node:stream';
+import type { RenderToPipeableStreamOptions } from 'react-dom/server';
+import { renderToPipeableStream } from 'react-dom/server';
+import type { AppLoadContext, EntryContext } from 'react-router';
+import { ServerRouter } from 'react-router';
+
+import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
+import { dynamicActivate, extractLocaleData } from '@documenso/lib/utils/i18n';
+
+import { langCookie } from './storage/lang-cookie.server';
+
+export const streamTimeout = 5_000;
+
+export default async function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ routerContext: EntryContext,
+ _loadContext: AppLoadContext,
+) {
+ let language = await langCookie.parse(request.headers.get('cookie') ?? '');
+
+ if (!APP_I18N_OPTIONS.supportedLangs.includes(language)) {
+ language = extractLocaleData({ headers: request.headers }).lang;
+ }
+
+ await dynamicActivate(language);
+
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const userAgent = request.headers.get('user-agent');
+
+ // Ensure requests from bots and SPA Mode renders wait for all content to load before responding
+ // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
+ const readyOption: keyof RenderToPipeableStreamOptions =
+ (userAgent && isbot(userAgent)) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady';
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [readyOption]() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set('Content-Type', 'text/html');
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ }),
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ },
+ );
+
+ // Abort the rendering stream after the `streamTimeout` so it has time to
+ // flush down the rejected boundaries
+ setTimeout(abort, streamTimeout + 1000);
+ });
+}
diff --git a/apps/web/src/providers/posthog.tsx b/apps/remix/app/providers/posthog.tsx
similarity index 62%
rename from apps/web/src/providers/posthog.tsx
rename to apps/remix/app/providers/posthog.tsx
index dd90c813b..eb3e7dd23 100644
--- a/apps/web/src/providers/posthog.tsx
+++ b/apps/remix/app/providers/posthog.tsx
@@ -1,36 +1,29 @@
-'use client';
-
import { useEffect } from 'react';
-import { usePathname, useSearchParams } from 'next/navigation';
-
-import { getSession } from 'next-auth/react';
import posthog from 'posthog-js';
+import { useLocation, useSearchParams } from 'react-router';
+import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
export function PostHogPageview() {
const postHogConfig = extractPostHogConfig();
- const pathname = usePathname();
- const searchParams = useSearchParams();
+ const { pathname } = useLocation();
+ const [searchParams] = useSearchParams();
+
+ const { user } = useOptionalSession();
if (typeof window !== 'undefined' && postHogConfig) {
posthog.init(postHogConfig.key, {
api_host: postHogConfig.host,
disable_session_recording: true,
loaded: () => {
- getSession()
- .then((session) => {
- if (session) {
- posthog.identify(session.user.email ?? session.user.id.toString());
- } else {
- posthog.reset();
- }
- })
- .catch(() => {
- // Do nothing.
- });
+ if (user) {
+ posthog.identify(user.email ?? user.id.toString());
+ } else {
+ posthog.reset();
+ }
},
custom_campaign_params: ['src'],
});
diff --git a/apps/web/src/providers/team.tsx b/apps/remix/app/providers/team.tsx
similarity index 74%
rename from apps/web/src/providers/team.tsx
rename to apps/remix/app/providers/team.tsx
index 88455d475..7fa250458 100644
--- a/apps/web/src/providers/team.tsx
+++ b/apps/remix/app/providers/team.tsx
@@ -1,16 +1,14 @@
-'use client';
-
import { createContext, useContext } from 'react';
import React from 'react';
-import type { TGetTeamByIdResponse } from '@documenso/lib/server-only/team/get-team';
+import type { TGetTeamByUrlResponse } from '@documenso/lib/server-only/team/get-team';
interface TeamProviderProps {
children: React.ReactNode;
- team: TGetTeamByIdResponse;
+ team: TGetTeamByUrlResponse;
}
-const TeamContext = createContext(null);
+const TeamContext = createContext(null);
export const useCurrentTeam = () => {
const context = useContext(TeamContext);
diff --git a/apps/remix/app/root.tsx b/apps/remix/app/root.tsx
new file mode 100644
index 000000000..caee42f7d
--- /dev/null
+++ b/apps/remix/app/root.tsx
@@ -0,0 +1,158 @@
+import { Suspense, useEffect } from 'react';
+
+import Plausible from 'plausible-tracker';
+import {
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+ data,
+ isRouteErrorResponse,
+ useLoaderData,
+ useLocation,
+} from 'react-router';
+import { ThemeProvider } from 'remix-themes';
+import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
+
+import { SessionProvider } from '@documenso/lib/client-only/providers/session';
+import { APP_I18N_OPTIONS, type SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
+import { createPublicEnv } from '@documenso/lib/utils/env';
+import { extractLocaleData } from '@documenso/lib/utils/i18n';
+import { TrpcProvider } from '@documenso/trpc/react';
+import { Toaster } from '@documenso/ui/primitives/toaster';
+import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
+
+import type { Route } from './+types/root';
+import stylesheet from './app.css?url';
+import { GenericErrorLayout } from './components/general/generic-error-layout';
+import { RefreshOnFocus } from './components/general/refresh-on-focus';
+import { PostHogPageview } from './providers/posthog';
+import { langCookie } from './storage/lang-cookie.server';
+import { themeSessionResolver } from './storage/theme-session.server';
+import { appMetaTags } from './utils/meta';
+
+const { trackPageview } = Plausible({
+ domain: 'documenso.com',
+});
+
+export const links: Route.LinksFunction = () => [
+ { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
+ {
+ rel: 'preconnect',
+ href: 'https://fonts.gstatic.com',
+ crossOrigin: 'anonymous',
+ },
+ {
+ rel: 'stylesheet',
+ href: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400..600&display=swap',
+ },
+ {
+ rel: 'stylesheet',
+ href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
+ },
+ { rel: 'stylesheet', href: stylesheet },
+];
+
+export function meta() {
+ return appMetaTags();
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const session = getOptionalLoaderSession();
+
+ const { getTheme } = await themeSessionResolver(request);
+
+ let lang: SupportedLanguageCodes = await langCookie.parse(request.headers.get('cookie') ?? '');
+
+ if (!APP_I18N_OPTIONS.supportedLangs.includes(lang)) {
+ lang = extractLocaleData({ headers: request.headers }).lang;
+ }
+
+ return data(
+ {
+ lang,
+ theme: getTheme(),
+ session,
+ publicEnv: createPublicEnv(),
+ },
+ {
+ headers: {
+ 'Set-Cookie': await langCookie.serialize(lang),
+ },
+ },
+ );
+}
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ const { publicEnv, theme, lang } = useLoaderData() || {};
+
+ // const [theme] = useTheme();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* */}
+
+
+
+
+
+
+ {children}
+
+
+
+ {/* Todo: Do we want this here? */}
+
+
+
+
+
+ );
+}
+
+export default function App({ loaderData }: Route.ComponentProps) {
+ const location = useLocation();
+
+ useEffect(() => {
+ trackPageview();
+ }, [location.pathname]);
+
+ return (
+
+ {/* Todo: Themes (this won't work for now) */}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
+ console.error('[RootErrorBoundary]', error);
+
+ const errorCode = isRouteErrorResponse(error) ? error.status : 500;
+
+ return ;
+}
diff --git a/apps/remix/app/routes.ts b/apps/remix/app/routes.ts
new file mode 100644
index 000000000..1ab599792
--- /dev/null
+++ b/apps/remix/app/routes.ts
@@ -0,0 +1,13 @@
+import { remixRoutesOptionAdapter } from '@react-router/remix-routes-option-adapter';
+import { flatRoutes } from 'remix-flat-routes';
+
+export default remixRoutesOptionAdapter((defineRoutes) => {
+ return flatRoutes('routes', defineRoutes, {
+ ignoredRouteFiles: ['**/.*'], // Ignore dot files (like .DS_Store)
+ //appDir: 'app',
+ //routeDir: 'routes',
+ //basePath: '/',
+ //paramPrefixChar: '$',
+ //routeRegex: /(([+][\/\\][^\/\\:?*]+)|[\/\\]((index|route|layout|page)|(_[^\/\\:?*]+)|([^\/\\:?*]+\.route)))\.(ts|tsx|js|jsx|md|mdx)$$/,
+ });
+});
diff --git a/apps/remix/app/routes/_authenticated+/_layout.tsx b/apps/remix/app/routes/_authenticated+/_layout.tsx
new file mode 100644
index 000000000..983e8eb88
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/_layout.tsx
@@ -0,0 +1,64 @@
+import { SubscriptionStatus } from '@prisma/client';
+import { Outlet } from 'react-router';
+import { getLoaderSession } from 'server/utils/get-loader-session';
+
+import { getLimits } from '@documenso/ee/server-only/limits/client';
+import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
+import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
+import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
+
+import { AppBanner } from '~/components/general/app-banner';
+import { Header } from '~/components/general/app-header';
+import { TeamLayoutBillingBanner } from '~/components/general/teams/team-layout-billing-banner';
+import { VerifyEmailBanner } from '~/components/general/verify-email-banner';
+
+import type { Route } from './+types/_layout';
+
+export const loader = async ({ request }: Route.LoaderArgs) => {
+ const { user, teams, currentTeam } = getLoaderSession();
+
+ const requestHeaders = Object.fromEntries(request.headers.entries());
+
+ // Todo: Should only load this on first render.
+ const [limits, banner] = await Promise.all([
+ getLimits({ headers: requestHeaders, teamId: currentTeam?.id }),
+ getSiteSettings().then((settings) =>
+ settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
+ ),
+ ]);
+
+ return {
+ user,
+ teams,
+ banner,
+ limits,
+ currentTeam,
+ };
+};
+
+export default function Layout({ loaderData }: Route.ComponentProps) {
+ const { user, teams, banner, limits, currentTeam } = loaderData;
+
+ return (
+
+ {!user.emailVerified && }
+
+ {currentTeam?.subscription &&
+ currentTeam.subscription.status !== SubscriptionStatus.ACTIVE && (
+
+ )}
+
+ {banner && }
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/remix/app/routes/_authenticated+/admin+/_index.tsx b/apps/remix/app/routes/_authenticated+/admin+/_index.tsx
new file mode 100644
index 000000000..d17a970f5
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/admin+/_index.tsx
@@ -0,0 +1,9 @@
+import { redirect } from 'react-router';
+
+export function loader() {
+ throw redirect('/admin/stats');
+}
+
+export default function AdminPage() {
+ // Redirect page.
+}
diff --git a/apps/remix/app/routes/_authenticated+/admin+/_layout.tsx b/apps/remix/app/routes/_authenticated+/admin+/_layout.tsx
new file mode 100644
index 000000000..9ae357270
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/admin+/_layout.tsx
@@ -0,0 +1,120 @@
+import { Trans } from '@lingui/react/macro';
+import { BarChart3, FileStack, Settings, Trophy, Users, Wallet2 } from 'lucide-react';
+import { Link, Outlet, redirect, useLocation } from 'react-router';
+import { getLoaderSession } from 'server/utils/get-loader-session';
+
+import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+
+export function loader() {
+ const { user } = getLoaderSession();
+
+ if (!user || !isAdmin(user)) {
+ throw redirect('/documents');
+ }
+}
+
+export default function AdminLayout() {
+ const { pathname } = useLocation();
+
+ return (
+
+
+
+
+
+
+ Stats
+
+
+
+
+
+
+ Users
+
+
+
+
+
+
+ Documents
+
+
+
+
+
+
+ Subscriptions
+
+
+
+
+
+
+ Leaderboard
+
+
+
+
+
+
+ Site Settings
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx b/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
new file mode 100644
index 000000000..4215573da
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@@ -0,0 +1,165 @@
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { SigningStatus } from '@prisma/client';
+import { DateTime } from 'luxon';
+import { Link, redirect } from 'react-router';
+
+import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
+import { trpc } from '@documenso/trpc/react';
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from '@documenso/ui/primitives/accordion';
+import { Badge } from '@documenso/ui/primitives/badge';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@documenso/ui/primitives/tooltip';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { AdminDocumentDeleteDialog } from '~/components/dialogs/admin-document-delete-dialog';
+import { DocumentStatus } from '~/components/general/document/document-status';
+import { AdminDocumentRecipientItemTable } from '~/components/tables/admin-document-recipient-item-table';
+
+import type { Route } from './+types/documents.$id';
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const id = Number(params.id);
+
+ if (isNaN(id)) {
+ throw redirect('/admin/documents');
+ }
+
+ const document = await getEntireDocument({ id });
+
+ return { document };
+}
+
+export default function AdminDocumentDetailsPage({ loaderData }: Route.ComponentProps) {
+ const { document } = loaderData;
+
+ const { _, i18n } = useLingui();
+ const { toast } = useToast();
+
+ const { mutate: resealDocument, isPending: isResealDocumentLoading } =
+ trpc.admin.resealDocument.useMutation({
+ onSuccess: () => {
+ toast({
+ title: _(msg`Success`),
+ description: _(msg`Document resealed`),
+ });
+ },
+ onError: () => {
+ toast({
+ title: _(msg`Error`),
+ description: _(msg`Failed to reseal document`),
+ variant: 'destructive',
+ });
+ },
+ });
+
+ return (
+
+
+
+
{document.title}
+
+
+
+ {document.deletedAt && (
+
+ Deleted
+
+ )}
+
+
+
+
+ Created on : {i18n.date(document.createdAt, DateTime.DATETIME_MED)}
+
+
+
+ Last updated at : {i18n.date(document.updatedAt, DateTime.DATETIME_MED)}
+
+
+
+
+
+
+ Admin Actions
+
+
+
+
+
+
+ recipient.signingStatus !== SigningStatus.SIGNED,
+ )}
+ onClick={() => resealDocument({ id: document.id })}
+ >
+ Reseal document
+
+
+
+
+
+ Attempts sealing the document again, useful for after a code change has occurred to
+ resolve an erroneous document.
+
+
+
+
+
+
+
+ Go to owner
+
+
+
+
+
+
+ Recipients
+
+
+
+
+ {document.recipients.map((recipient) => (
+
+
+
+
{recipient.name}
+
+ {recipient.email}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ {document &&
}
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx b/apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
similarity index 73%
rename from apps/web/src/app/(dashboard)/admin/documents/document-results.tsx
rename to apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
index 98854f296..3f37ca728 100644
--- a/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx
+++ b/apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
@@ -1,13 +1,10 @@
-'use client';
-
import { useMemo, useState } from 'react';
-import Link from 'next/link';
-import { useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
+import { Link, useSearchParams } from 'react-router';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
@@ -20,14 +17,12 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Input } from '@documenso/ui/primitives/input';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-import { DocumentStatus } from '~/components/formatter/document-status';
+import { DocumentStatus } from '~/components/general/document/document-status';
-// export type AdminDocumentResultsProps = {};
-
-export const AdminDocumentResults = () => {
+export default function AdminDocumentsPage() {
const { _, i18n } = useLingui();
- const searchParams = useSearchParams();
+ const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
@@ -69,7 +64,7 @@ export const AdminDocumentResults = () => {
cell: ({ row }) => {
return (
{row.original.title}
@@ -93,7 +88,7 @@ export const AdminDocumentResults = () => {
return (
-
+
{avatarFallbackText}
@@ -135,31 +130,41 @@ export const AdminDocumentResults = () => {
return (
-
setTerm(e.target.value)}
- />
+
+ Manage documents
+
-
-
- {(table) => }
-
+
+
+
setTerm(e.target.value)}
+ />
- {isFindDocumentsLoading && (
-
-
+
+
+ {(table) => (
+
+ )}
+
+
+ {isFindDocumentsLoading && (
+
+
+
+ )}
- )}
+
);
-};
+}
diff --git a/apps/remix/app/routes/_authenticated+/admin+/leaderboard.tsx b/apps/remix/app/routes/_authenticated+/admin+/leaderboard.tsx
new file mode 100644
index 000000000..a258a21aa
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/admin+/leaderboard.tsx
@@ -0,0 +1,66 @@
+import { Trans } from '@lingui/react/macro';
+
+import { getSigningVolume } from '@documenso/lib/server-only/admin/get-signing-volume';
+
+import { AdminLeaderboardTable } from '~/components/tables/admin-leaderboard-table';
+
+import type { Route } from './+types/leaderboard';
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const url = new URL(request.url);
+
+ const rawSortBy = url.searchParams.get('sortBy') || 'signingVolume';
+ const rawSortOrder = url.searchParams.get('sortOrder') || 'desc';
+
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+ const sortOrder = (['asc', 'desc'].includes(rawSortOrder) ? rawSortOrder : 'desc') as
+ | 'asc'
+ | 'desc';
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+ const sortBy = (
+ ['name', 'createdAt', 'signingVolume'].includes(rawSortBy) ? rawSortBy : 'signingVolume'
+ ) as 'name' | 'createdAt' | 'signingVolume';
+
+ const page = Number(url.searchParams.get('page')) || 1;
+ const perPage = Number(url.searchParams.get('perPage')) || 10;
+ const search = url.searchParams.get('search') || '';
+
+ const { leaderboard: signingVolume, totalPages } = await getSigningVolume({
+ search,
+ page,
+ perPage,
+ sortBy,
+ sortOrder,
+ });
+
+ return {
+ signingVolume,
+ totalPages,
+ page,
+ perPage,
+ sortBy,
+ sortOrder,
+ };
+}
+
+export default function Leaderboard({ loaderData }: Route.ComponentProps) {
+ const { signingVolume, totalPages, page, perPage, sortBy, sortOrder } = loaderData;
+
+ return (
+
+ );
+}
diff --git a/apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx b/apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
new file mode 100644
index 000000000..8318785eb
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
@@ -0,0 +1,224 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { useForm } from 'react-hook-form';
+import { useRevalidator } from 'react-router';
+import type { z } from 'zod';
+
+import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
+import {
+ SITE_SETTINGS_BANNER_ID,
+ ZSiteSettingsBannerSchema,
+} from '@documenso/lib/server-only/site-settings/schemas/banner';
+import { trpc as trpcReact } from '@documenso/trpc/react';
+import { Button } from '@documenso/ui/primitives/button';
+import { ColorPicker } from '@documenso/ui/primitives/color-picker';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Switch } from '@documenso/ui/primitives/switch';
+import { Textarea } from '@documenso/ui/primitives/textarea';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { SettingsHeader } from '~/components/general/settings-header';
+
+import type { Route } from './+types/site-settings';
+
+const ZBannerFormSchema = ZSiteSettingsBannerSchema;
+
+type TBannerFormSchema = z.infer
;
+
+export async function loader() {
+ const banner = await getSiteSettings().then((settings) =>
+ settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
+ );
+
+ return { banner };
+}
+
+export default function AdminBannerPage({ loaderData }: Route.ComponentProps) {
+ const { banner } = loaderData;
+
+ const { toast } = useToast();
+ const { _ } = useLingui();
+ const { revalidate } = useRevalidator();
+
+ const form = useForm({
+ resolver: zodResolver(ZBannerFormSchema),
+ defaultValues: {
+ id: SITE_SETTINGS_BANNER_ID,
+ enabled: banner?.enabled ?? false,
+ data: {
+ content: banner?.data?.content ?? '',
+ bgColor: banner?.data?.bgColor ?? '#000000',
+ textColor: banner?.data?.textColor ?? '#FFFFFF',
+ },
+ },
+ });
+
+ const enabled = form.watch('enabled');
+
+ const { mutateAsync: updateSiteSetting, isPending: isUpdateSiteSettingLoading } =
+ trpcReact.admin.updateSiteSetting.useMutation();
+
+ const onBannerUpdate = async ({ id, enabled, data }: TBannerFormSchema) => {
+ try {
+ await updateSiteSetting({
+ id,
+ enabled,
+ data,
+ });
+
+ toast({
+ title: _(msg`Banner Updated`),
+ description: _(msg`Your banner has been updated successfully.`),
+ duration: 5000,
+ });
+
+ await revalidate();
+ } catch (err) {
+ toast({
+ title: _(msg`An unknown error occurred`),
+ variant: 'destructive',
+ description: _(
+ msg`We encountered an unknown error while attempting to update the banner. Please try again later.`,
+ ),
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Site Banner
+
+
+
+ The site banner is a message that is shown at the top of the site. It can be used to
+ display important information to your users.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/remix/app/routes/_authenticated+/admin+/stats.tsx
similarity index 76%
rename from apps/web/src/app/(dashboard)/admin/stats/page.tsx
rename to apps/remix/app/routes/_authenticated+/admin+/stats.tsx
index 9ffbfb5dc..e511e2caf 100644
--- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx
+++ b/apps/remix/app/routes/_authenticated+/admin+/stats.tsx
@@ -1,5 +1,6 @@
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import {
File,
FileCheck,
@@ -14,7 +15,6 @@ import {
Users,
} from 'lucide-react';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
import {
@@ -23,17 +23,15 @@ import {
getUsersWithSubscriptionsCount,
} from '@documenso/lib/server-only/admin/get-users-stats';
import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get-signer-conversion';
+import { env } from '@documenso/lib/utils/env';
-import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
+import { AdminStatsSignerConversionChart } from '~/components/general/admin-stats-signer-conversion-chart';
+import { AdminStatsUsersWithDocumentsChart } from '~/components/general/admin-stats-users-with-documents';
+import { CardMetric } from '~/components/general/metric-card';
-import { SignerConversionChart } from './signer-conversion-chart';
-import { UserWithDocumentChart } from './user-with-document';
-
-export default async function AdminStatsPage() {
- await setupI18nSSR();
-
- const { _ } = useLingui();
+import type { Route } from './+types/stats';
+export async function loader() {
const [
usersCount,
usersWithSubscriptionsCount,
@@ -54,6 +52,28 @@ export default async function AdminStatsPage() {
getUserWithSignedDocumentMonthlyGrowth(),
]);
+ return {
+ usersCount,
+ usersWithSubscriptionsCount,
+ docStats,
+ recipientStats,
+ signerConversionMonthly,
+ MONTHLY_USERS_SIGNED,
+ };
+}
+
+export default function AdminStatsPage({ loaderData }: Route.ComponentProps) {
+ const { _ } = useLingui();
+
+ const {
+ usersCount,
+ usersWithSubscriptionsCount,
+ docStats,
+ recipientStats,
+ signerConversionMonthly,
+ MONTHLY_USERS_SIGNED,
+ } = loaderData;
+
return (
@@ -69,11 +89,7 @@ export default async function AdminStatsPage() {
value={usersWithSubscriptionsCount}
/>
-
+
@@ -132,12 +148,12 @@ export default async function AdminStatsPage() {
Charts
-
-
-
-
+
@@ -68,7 +72,7 @@ export default async function Subscriptions() {
: 'N/A'}
- {subscription.userId}
+ {subscription.userId}
))}
diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
similarity index 84%
rename from apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx
rename to apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
index 371726de1..81d8dfbcc 100644
--- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx
+++ b/apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
@@ -1,11 +1,9 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
+import { useRevalidator } from 'react-router';
import type { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
@@ -22,10 +20,11 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { DeleteUserDialog } from './delete-user-dialog';
-import { DisableUserDialog } from './disable-user-dialog';
-import { EnableUserDialog } from './enable-user-dialog';
-import { MultiSelectRoleCombobox } from './multiselect-role-combobox';
+import { AdminUserDeleteDialog } from '~/components/dialogs/admin-user-delete-dialog';
+import { AdminUserDisableDialog } from '~/components/dialogs/admin-user-disable-dialog';
+import { AdminUserEnableDialog } from '~/components/dialogs/admin-user-enable-dialog';
+
+import { MultiSelectRoleCombobox } from '../../../components/general/multiselect-role-combobox';
const ZUserFormSchema = ZAdminUpdateProfileMutationSchema.omit({ id: true });
@@ -34,8 +33,7 @@ type TUserFormSchema = z.infer;
export default function UserPage({ params }: { params: { id: number } }) {
const { _ } = useLingui();
const { toast } = useToast();
-
- const router = useRouter();
+ const { revalidate } = useRevalidator();
const { data: user } = trpc.profile.getUser.useQuery(
{
@@ -68,7 +66,7 @@ export default function UserPage({ params }: { params: { id: number } }) {
roles,
});
- router.refresh();
+ await revalidate();
toast({
title: _(msg`Profile updated`),
@@ -156,9 +154,9 @@ export default function UserPage({ params }: { params: { id: number } }) {
- {user &&
}
- {user && user.disabled &&
}
- {user && !user.disabled &&
}
+ {user &&
}
+ {user && user.disabled &&
}
+ {user && !user.disabled &&
}
);
diff --git a/apps/remix/app/routes/_authenticated+/admin+/users._index.tsx b/apps/remix/app/routes/_authenticated+/admin+/users._index.tsx
new file mode 100644
index 000000000..f34bde4af
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/admin+/users._index.tsx
@@ -0,0 +1,52 @@
+import { Trans } from '@lingui/react/macro';
+
+import { getPricesByPlan } from '@documenso/ee/server-only/stripe/get-prices-by-plan';
+import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
+import { findUsers } from '@documenso/lib/server-only/user/get-all-users';
+
+import { AdminDashboardUsersTable } from '~/components/tables/admin-dashboard-users-table';
+
+import type { Route } from './+types/users._index';
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const url = new URL(request.url);
+
+ const page = Number(url.searchParams.get('page')) || 1;
+ const perPage = Number(url.searchParams.get('perPage')) || 10;
+ const search = url.searchParams.get('search') || '';
+
+ const [{ users, totalPages }, individualPrices] = await Promise.all([
+ findUsers({ username: search, email: search, page, perPage }),
+ getPricesByPlan([STRIPE_PLAN_TYPE.REGULAR, STRIPE_PLAN_TYPE.COMMUNITY]).catch(() => []),
+ ]);
+
+ const individualPriceIds = individualPrices.map((price) => price.id);
+
+ return {
+ users,
+ totalPages,
+ individualPriceIds,
+ page,
+ perPage,
+ };
+}
+
+export default function AdminManageUsersPage({ loaderData }: Route.ComponentProps) {
+ const { users, totalPages, individualPriceIds, page, perPage } = loaderData;
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx b/apps/remix/app/routes/_authenticated+/documents+/$id._index.tsx
similarity index 69%
rename from apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx
rename to apps/remix/app/routes/_authenticated+/documents+/$id._index.tsx
index 7892e6eba..d472bb9ee 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx
+++ b/apps/remix/app/routes/_authenticated+/documents+/$id._index.tsx
@@ -1,64 +1,52 @@
-import Link from 'next/link';
-import { redirect } from 'next/navigation';
-
-import { Plural, Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
+import { Plural, Trans } from '@lingui/react/macro';
+import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
+import { Link, redirect } from 'react-router';
+import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
-import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
+import { useSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
-import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
-import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import { DocumentStatus } from '@documenso/prisma/client';
-import type { Team, TeamEmail } from '@documenso/prisma/client';
-import { TeamMemberRole } from '@documenso/prisma/client';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
-import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
-import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
-import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
+import { DocumentHistorySheet } from '~/components/general/document/document-history-sheet';
+import { DocumentPageViewButton } from '~/components/general/document/document-page-view-button';
+import { DocumentPageViewDropdown } from '~/components/general/document/document-page-view-dropdown';
+import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
+import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity';
+import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients';
+import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
+import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
import {
DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP,
-} from '~/components/formatter/document-status';
+} from '~/components/general/document/document-status';
+import { StackAvatarsWithTooltip } from '~/components/general/stack-avatars-with-tooltip';
+import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
-import { DocumentPageViewButton } from './document-page-view-button';
-import { DocumentPageViewDropdown } from './document-page-view-dropdown';
-import { DocumentPageViewInformation } from './document-page-view-information';
-import { DocumentPageViewRecentActivity } from './document-page-view-recent-activity';
-import { DocumentPageViewRecipients } from './document-page-view-recipients';
+import type { Route } from './+types/$id._index';
-export type DocumentPageViewProps = {
- params: {
- id: string;
- };
- team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember: { role: TeamMemberRole } };
-};
+export async function loader({ params }: Route.LoaderArgs) {
+ const { user, currentTeam: team } = getLoaderSession();
-export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) => {
const { id } = params;
- const { _ } = useLingui();
const documentId = Number(id);
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
- redirect(documentRootPath);
+ throw redirect(documentRootPath);
}
- const { user } = await getRequiredServerComponentSession();
-
const document = await getDocumentById({
documentId,
userId: user.id,
@@ -66,7 +54,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
}).catch(() => null);
if (document?.teamId && !team?.url) {
- redirect(documentRootPath);
+ throw redirect(documentRootPath);
}
const documentVisibility = document?.visibility;
@@ -85,37 +73,35 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
.otherwise(() => false);
}
- const isDocumentHistoryEnabled = await getServerComponentFlag(
- 'app_document_page_view_history_sheet',
- );
-
if (!document || !document.documentData || (team && !canAccessDocument)) {
- redirect(documentRootPath);
+ throw redirect(documentRootPath);
}
if (team && !canAccessDocument) {
- redirect(documentRootPath);
+ throw redirect(documentRootPath);
}
- const { documentData, documentMeta } = document;
+ const { documentMeta } = document;
- if (documentMeta?.password) {
- const key = DOCUMENSO_ENCRYPTION_KEY;
+ // Todo: We don't handle encrypted files right.
+ // if (documentMeta?.password) {
+ // const key = DOCUMENSO_ENCRYPTION_KEY;
- if (!key) {
- throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
- }
+ // if (!key) {
+ // throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
+ // }
- const securePassword = Buffer.from(
- symmetricDecrypt({
- key,
- data: documentMeta.password,
- }),
- ).toString('utf-8');
+ // const securePassword = Buffer.from(
+ // symmetricDecrypt({
+ // key,
+ // data: documentMeta.password,
+ // }),
+ // ).toString('utf-8');
- documentMeta.password = securePassword;
- }
+ // documentMeta.password = securePassword;
+ // }
+ // Todo: Get full document instead???
const [recipients, fields] = await Promise.all([
getRecipientsForDocument({
documentId,
@@ -134,13 +120,33 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
recipients,
};
+ return superLoaderJson({
+ document: documentWithRecipients,
+ documentRootPath,
+ fields,
+ });
+}
+
+export default function DocumentPage() {
+ const loaderData = useSuperLoaderData
();
+
+ const { _ } = useLingui();
+ const { user } = useSession();
+
+ const { document, documentRootPath, fields } = loaderData;
+
+ const { recipients, documentData, documentMeta } = document;
+
+ // This was a feature flag. Leave to false since it's not ready.
+ const isDocumentHistoryEnabled = false;
+
return (
{document.status === DocumentStatus.PENDING && (
)}
-
+
Documents
@@ -219,7 +225,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
{_(FRIENDLY_STATUS_MAP[document.status].labelExtended)}
-
+
@@ -247,18 +253,15 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
-
+
{/* Document information section. */}
-
+
{/* Recipients section. */}
-
+
{/* Recent activity section. */}
@@ -267,4 +270,4 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
);
-};
+}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx b/apps/remix/app/routes/_authenticated+/documents+/$id.edit.tsx
similarity index 63%
rename from apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx
rename to apps/remix/app/routes/_authenticated+/documents+/$id.edit.tsx
index 70e3323e2..e3f250aba 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx
+++ b/apps/remix/app/routes/_authenticated+/documents+/$id.edit.tsx
@@ -1,33 +1,25 @@
-import Link from 'next/link';
-import { redirect } from 'next/navigation';
-
-import { Plural, Trans } from '@lingui/macro';
+import { Plural, Trans } from '@lingui/react/macro';
+import { DocumentStatus as InternalDocumentStatus, TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Users2 } from 'lucide-react';
+import { Link, redirect } from 'react-router';
+import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
-import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
-import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Team } from '@documenso/prisma/client';
-import { TeamMemberRole } from '@documenso/prisma/client';
-import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
-import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
-import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
-import { DocumentStatus } from '~/components/formatter/document-status';
+import { DocumentEditForm } from '~/components/general/document/document-edit-form';
+import { DocumentStatus } from '~/components/general/document/document-status';
+import { StackAvatarsWithTooltip } from '~/components/general/stack-avatars-with-tooltip';
+import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
-export type DocumentEditPageViewProps = {
- params: {
- id: string;
- };
- team?: Team & { currentTeamMember: { role: TeamMemberRole } };
-};
+import type { Route } from './+types/$id.edit';
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const { user, currentTeam: team } = getLoaderSession();
-export const DocumentEditPageView = async ({ params, team }: DocumentEditPageViewProps) => {
const { id } = params;
const documentId = Number(id);
@@ -35,11 +27,9 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
- redirect(documentRootPath);
+ throw redirect(documentRootPath);
}
- const { user } = await getRequiredServerComponentSession();
-
const document = await getDocumentWithDetailsById({
documentId,
userId: user.id,
@@ -47,7 +37,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
}).catch(() => null);
if (document?.teamId && !team?.url) {
- redirect(documentRootPath);
+ throw redirect(documentRootPath);
}
const documentVisibility = document?.visibility;
@@ -67,44 +57,55 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
}
if (!document) {
- redirect(documentRootPath);
+ throw redirect(documentRootPath);
}
if (team && !canAccessDocument) {
- redirect(documentRootPath);
+ throw redirect(documentRootPath);
}
if (document.status === InternalDocumentStatus.COMPLETED) {
- redirect(`${documentRootPath}/${documentId}`);
+ throw redirect(`${documentRootPath}/${documentId}`);
}
- const { documentMeta, recipients } = document;
+ // Todo: We don't handle encrypted files right.
+ // if (documentMeta?.password) {
+ // const key = DOCUMENSO_ENCRYPTION_KEY;
- if (documentMeta?.password) {
- const key = DOCUMENSO_ENCRYPTION_KEY;
+ // if (!key) {
+ // throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
+ // }
- if (!key) {
- throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
- }
+ // const securePassword = Buffer.from(
+ // symmetricDecrypt({
+ // key,
+ // data: documentMeta.password,
+ // }),
+ // ).toString('utf-8');
- const securePassword = Buffer.from(
- symmetricDecrypt({
- key,
- data: documentMeta.password,
- }),
- ).toString('utf-8');
-
- documentMeta.password = securePassword;
- }
+ // documentMeta.password = securePassword;
+ // }
const isDocumentEnterprise = await isUserEnterprise({
userId: user.id,
teamId: team?.id,
});
+ return superLoaderJson({
+ document,
+ documentRootPath,
+ isDocumentEnterprise,
+ });
+}
+
+export default function DocumentEditPage() {
+ const { document, documentRootPath, isDocumentEnterprise } = useSuperLoaderData
();
+
+ const { recipients } = document;
+
return (
-
+
Documents
@@ -136,7 +137,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
)}
-
);
-};
+}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx b/apps/remix/app/routes/_authenticated+/documents+/$id.logs.tsx
similarity index 77%
rename from apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx
rename to apps/remix/app/routes/_authenticated+/documents+/$id.logs.tsx
index 877dad583..17144b080 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx
+++ b/apps/remix/app/routes/_authenticated+/documents+/$id.logs.tsx
@@ -1,50 +1,42 @@
-import Link from 'next/link';
-import { redirect } from 'next/navigation';
-
import type { MessageDescriptor } from '@lingui/core';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { Recipient } from '@prisma/client';
import { ChevronLeft } from 'lucide-react';
import { DateTime } from 'luxon';
+import { Link, redirect } from 'react-router';
+import { getLoaderSession } from 'server/utils/get-loader-session';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Recipient, Team } from '@documenso/prisma/client';
import { Card } from '@documenso/ui/primitives/card';
+import { DocumentAuditLogDownloadButton } from '~/components/general/document/document-audit-log-download-button';
+import { DocumentCertificateDownloadButton } from '~/components/general/document/document-certificate-download-button';
import {
DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP,
-} from '~/components/formatter/document-status';
+} from '~/components/general/document/document-status';
+import { DocumentLogsTable } from '~/components/tables/document-logs-table';
-import { DocumentLogsDataTable } from './document-logs-data-table';
-import { DownloadAuditLogButton } from './download-audit-log-button';
-import { DownloadCertificateButton } from './download-certificate-button';
-
-export type DocumentLogsPageViewProps = {
- params: {
- id: string;
- };
- team?: Team;
-};
-
-export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => {
- const { _, i18n } = useLingui();
+import type { Route } from './+types/$id.logs';
+export async function loader({ params }: Route.LoaderArgs) {
const { id } = params;
+ const { user, currentTeam: team } = getLoaderSession();
+
const documentId = Number(id);
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
- redirect(documentRootPath);
+ throw redirect(documentRootPath);
}
- const { user } = await getRequiredServerComponentSession();
-
+ // Todo: Get detailed?
const [document, recipients] = await Promise.all([
getDocumentById({
documentId,
@@ -59,9 +51,21 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
]);
if (!document || !document.documentData) {
- redirect(documentRootPath);
+ throw redirect(documentRootPath);
}
+ return {
+ document,
+ documentRootPath,
+ recipients,
+ };
+}
+
+export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
+ const { document, documentRootPath, recipients } = loaderData;
+
+ const { _, i18n } = useLingui();
+
const documentInformation: { description: MessageDescriptor; value: string }[] = [
{
description: msg`Document title`,
@@ -108,11 +112,10 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
return `[${recipient.role}] ${text}`;
};
-
return (
@@ -138,14 +141,13 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
-
-
+
@@ -172,8 +174,8 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
);
-};
+}
diff --git a/apps/remix/app/routes/_authenticated+/documents+/_index.tsx b/apps/remix/app/routes/_authenticated+/documents+/_index.tsx
new file mode 100644
index 000000000..6c5ced573
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/documents+/_index.tsx
@@ -0,0 +1,156 @@
+import { useEffect, useMemo, useState } from 'react';
+
+import { Trans } from '@lingui/react/macro';
+import { useSearchParams } from 'react-router';
+import { Link } from 'react-router';
+import { z } from 'zod';
+
+import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
+import { parseToIntegerArray } from '@documenso/lib/utils/params';
+import { formatDocumentsPath } from '@documenso/lib/utils/teams';
+import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
+import { trpc } from '@documenso/trpc/react';
+import {
+ type TFindDocumentsInternalResponse,
+ ZFindDocumentsInternalRequestSchema,
+} from '@documenso/trpc/server/document-router/schema';
+import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
+import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
+
+import { DocumentSearch } from '~/components/general/document/document-search';
+import { DocumentStatus } from '~/components/general/document/document-status';
+import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
+import { PeriodSelector } from '~/components/general/period-selector';
+import { DocumentsTable } from '~/components/tables/documents-table';
+import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state';
+import { DocumentsTableSenderFilter } from '~/components/tables/documents-table-sender-filter';
+import { useOptionalCurrentTeam } from '~/providers/team';
+import { appMetaTags } from '~/utils/meta';
+
+export function meta() {
+ return appMetaTags('Documents');
+}
+
+const ZSearchParamsSchema = ZFindDocumentsInternalRequestSchema.pick({
+ status: true,
+ period: true,
+ page: true,
+ perPage: true,
+ query: true,
+}).extend({
+ senderIds: z.string().transform(parseToIntegerArray).optional().catch([]),
+});
+
+export default function DocumentsPage() {
+ const [searchParams] = useSearchParams();
+
+ const team = useOptionalCurrentTeam();
+
+ const [stats, setStats] = useState({
+ [ExtendedDocumentStatus.DRAFT]: 0,
+ [ExtendedDocumentStatus.PENDING]: 0,
+ [ExtendedDocumentStatus.COMPLETED]: 0,
+ [ExtendedDocumentStatus.INBOX]: 0,
+ [ExtendedDocumentStatus.ALL]: 0,
+ });
+
+ const findDocumentSearchParams = useMemo(
+ () => ZSearchParamsSchema.safeParse(Object.fromEntries(searchParams.entries())).data || {},
+ [searchParams],
+ );
+
+ const { data, isLoading, isLoadingError } = trpc.document.findDocumentsInternal.useQuery({
+ ...findDocumentSearchParams,
+ });
+
+ const getTabHref = (value: keyof typeof ExtendedDocumentStatus) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('status', value);
+
+ if (params.has('page')) {
+ params.delete('page');
+ }
+
+ return `${formatDocumentsPath(team?.url)}?${params.toString()}`;
+ };
+
+ useEffect(() => {
+ if (data?.stats) {
+ setStats(data.stats);
+ }
+ }, [data?.stats]);
+
+ return (
+
+
+
+
+
+ {team && (
+
+ {team.avatarImageId && }
+
+ {team.name.slice(0, 1)}
+
+
+ )}
+
+
+ Documents
+
+
+
+
+
+
+ {[
+ ExtendedDocumentStatus.INBOX,
+ ExtendedDocumentStatus.PENDING,
+ ExtendedDocumentStatus.COMPLETED,
+ ExtendedDocumentStatus.DRAFT,
+ ExtendedDocumentStatus.ALL,
+ ].map((value) => (
+
+
+
+
+ {value !== ExtendedDocumentStatus.ALL && (
+ {stats[value]}
+ )}
+
+
+ ))}
+
+
+
+ {team &&
}
+
+
+
+
+
+
+
+
+
+
+ {data && data.count === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/loading.tsx b/apps/remix/app/routes/_authenticated+/documents+/todo-loading.tsx
similarity index 76%
rename from apps/web/src/app/(dashboard)/documents/[id]/loading.tsx
rename to apps/remix/app/routes/_authenticated+/documents+/todo-loading.tsx
index b6165436b..8be730b30 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/loading.tsx
+++ b/apps/remix/app/routes/_authenticated+/documents+/todo-loading.tsx
@@ -1,17 +1,13 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
import { ChevronLeft, Loader } from 'lucide-react';
+import { Link } from 'react-router';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
-export default async function Loading() {
- await setupI18nSSR();
-
+export default function Loading() {
return (
-
+
Documents
@@ -35,7 +31,7 @@ export default async function Loading() {
-
+
);
diff --git a/apps/remix/app/routes/_authenticated+/settings+/_index.tsx b/apps/remix/app/routes/_authenticated+/settings+/_index.tsx
new file mode 100644
index 000000000..f66113db0
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/settings+/_index.tsx
@@ -0,0 +1,5 @@
+import { redirect } from 'react-router';
+
+export function loader() {
+ throw redirect('/settings/profile');
+}
diff --git a/apps/remix/app/routes/_authenticated+/settings+/_layout.tsx b/apps/remix/app/routes/_authenticated+/settings+/_layout.tsx
new file mode 100644
index 000000000..c1df197e9
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/settings+/_layout.tsx
@@ -0,0 +1,29 @@
+import { Trans } from '@lingui/react/macro';
+import { Outlet } from 'react-router';
+
+import { SettingsDesktopNav } from '~/components/general/settings-nav-desktop';
+import { SettingsMobileNav } from '~/components/general/settings-nav-mobile';
+import { appMetaTags } from '~/utils/meta';
+
+export function meta() {
+ return appMetaTags('Settings');
+}
+
+export default function SettingsLayout() {
+ return (
+
+ );
+}
diff --git a/apps/remix/app/routes/_authenticated+/settings+/profile.tsx b/apps/remix/app/routes/_authenticated+/settings+/profile.tsx
new file mode 100644
index 000000000..77909d147
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/settings+/profile.tsx
@@ -0,0 +1,32 @@
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react';
+
+import { AccountDeleteDialog } from '~/components/dialogs/account-delete-dialog';
+import { AvatarImageForm } from '~/components/forms/avatar-image';
+import { ProfileForm } from '~/components/forms/profile';
+import { SettingsHeader } from '~/components/general/settings-header';
+import { appMetaTags } from '~/utils/meta';
+
+export function meta() {
+ return appMetaTags('Profile');
+}
+
+export default function SettingsProfile() {
+ const { _ } = useLingui();
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/settings/public-profile/public-profile-page-view.tsx b/apps/remix/app/routes/_authenticated+/settings+/public-profile+/index.tsx
similarity index 83%
rename from apps/web/src/app/(dashboard)/settings/public-profile/public-profile-page-view.tsx
rename to apps/remix/app/routes/_authenticated+/settings+/public-profile+/index.tsx
index 0795c29ca..2f3fa5f2d 100644
--- a/apps/web/src/app/(dashboard)/settings/public-profile/public-profile-page-view.tsx
+++ b/apps/remix/app/routes/_authenticated+/settings+/public-profile+/index.tsx
@@ -1,18 +1,14 @@
-'use client';
-
import { useEffect, useMemo, useState } from 'react';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { TemplateDirectLink } from '@prisma/client';
+import { TemplateType } from '@prisma/client';
+import { getLoaderSession } from 'server/utils/get-loader-session';
-import type {
- Team,
- TeamProfile,
- TemplateDirectLink,
- User,
- UserProfile,
-} from '@documenso/prisma/client';
-import { TemplateType } from '@documenso/prisma/client';
+import { useSession } from '@documenso/lib/client-only/providers/session';
+import { getUserPublicProfile } from '@documenso/lib/server-only/user/get-user-public-profile';
import { trpc } from '@documenso/trpc/react';
import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
import { cn } from '@documenso/ui/lib/utils';
@@ -21,18 +17,14 @@ import { Switch } from '@documenso/ui/primitives/switch';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
+import { ManagePublicTemplateDialog } from '~/components/dialogs/public-profile-template-manage-dialog';
import type { TPublicProfileFormSchema } from '~/components/forms/public-profile-form';
import { PublicProfileForm } from '~/components/forms/public-profile-form';
-import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
+import { SettingsHeader } from '~/components/general/settings-header';
+import { useOptionalCurrentTeam } from '~/providers/team';
-import { PublicTemplatesDataTable } from './public-templates-data-table';
-
-export type PublicProfilePageViewOptions = {
- user: User;
- team?: Team;
- profile: UserProfile | TeamProfile;
-};
+import { SettingsPublicProfileTemplatesTable } from '../../../../components/tables/settings-public-profile-templates-table';
+import type { Route } from './+types/index';
type DirectTemplate = FindTemplateRow & {
directLink: Pick;
@@ -52,10 +44,25 @@ const teamProfileText = {
templatesSubtitle: msg`Show templates in your team public profile for your audience to sign and get started quickly`,
};
-export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePageViewOptions) => {
+export async function loader() {
+ const { user } = getLoaderSession();
+
+ const { profile } = await getUserPublicProfile({
+ userId: user.id,
+ });
+
+ return { profile };
+}
+
+export default function PublicProfilePage({ loaderData }: Route.ComponentProps) {
+ const { profile } = loaderData;
+
const { _ } = useLingui();
const { toast } = useToast();
+ const { user } = useSession();
+ const team = useOptionalCurrentTeam();
+
const [isPublicProfileVisible, setIsPublicProfileVisible] = useState(profile.enabled);
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
@@ -219,9 +226,9 @@ export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePage
);
-};
+}
diff --git a/apps/remix/app/routes/_authenticated+/settings+/security+/activity+/index.tsx b/apps/remix/app/routes/_authenticated+/settings+/security+/activity+/index.tsx
new file mode 100644
index 000000000..7156ee09e
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/settings+/security+/activity+/index.tsx
@@ -0,0 +1,28 @@
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react';
+
+import { SettingsHeader } from '~/components/general/settings-header';
+import { SettingsSecurityActivityTable } from '~/components/tables/settings-security-activity-table';
+import { appMetaTags } from '~/utils/meta';
+
+export function meta() {
+ return appMetaTags('Security activity');
+}
+
+export default function SettingsSecurityActivity() {
+ const { _ } = useLingui();
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/settings/security/page.tsx b/apps/remix/app/routes/_authenticated+/settings+/security+/index.tsx
similarity index 62%
rename from apps/web/src/app/(dashboard)/settings/security/page.tsx
rename to apps/remix/app/routes/_authenticated+/settings+/security+/index.tsx
index 1f3d4de08..8e505ed78 100644
--- a/apps/web/src/app/(dashboard)/settings/security/page.tsx
+++ b/apps/remix/app/routes/_authenticated+/settings+/security+/index.tsx
@@ -1,32 +1,53 @@
-import type { Metadata } from 'next';
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { Link } from 'react-router';
+import { getLoaderSession } from 'server/utils/get-loader-session';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
+import { useSession } from '@documenso/lib/client-only/providers/session';
+import { prisma } from '@documenso/prisma';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { DisableAuthenticatorAppDialog } from '~/components/forms/2fa/disable-authenticator-app-dialog';
import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';
import { ViewRecoveryCodesDialog } from '~/components/forms/2fa/view-recovery-codes-dialog';
import { PasswordForm } from '~/components/forms/password';
+import { SettingsHeader } from '~/components/general/settings-header';
+import { appMetaTags } from '~/utils/meta';
-export const metadata: Metadata = {
- title: 'Security',
-};
+import type { Route } from './+types';
-export default async function SecuritySettingsPage() {
- await setupI18nSSR();
+export function meta() {
+ return appMetaTags('Security');
+}
+
+export async function loader() {
+ const { user } = getLoaderSession();
+
+ const accounts = await prisma.account.findMany({
+ where: {
+ userId: user.id,
+ },
+ select: {
+ provider: true,
+ },
+ });
+
+ const providers = accounts.map((account) => account.provider);
+
+ return {
+ providers,
+ };
+}
+
+export default function SettingsSecurity({ loaderData }: Route.ComponentProps) {
+ const { providers } = loaderData;
const { _ } = useLingui();
- const { user } = await getRequiredServerComponentSession();
+ const { user } = useSession();
- const isPasskeyEnabled = await getServerComponentFlag('app_passkey');
+ const hasEmailPasswordAccount = providers.includes('DOCUMENSO');
return (
@@ -34,8 +55,7 @@ export default async function SecuritySettingsPage() {
title={_(msg`Security`)}
subtitle={_(msg`Here you can manage your password and security settings.`)}
/>
-
- {user.identityProvider === 'DOCUMENSO' && (
+ {hasEmailPasswordAccount && (
<>
@@ -53,7 +73,7 @@ export default async function SecuritySettingsPage() {
- {user.identityProvider === 'DOCUMENSO' ? (
+ {hasEmailPasswordAccount ? (
Add an authenticator to serve as a secondary authentication method when signing in,
or when signing documents.
@@ -96,30 +116,28 @@ export default async function SecuritySettingsPage() {
)}
- {isPasskeyEnabled && (
-
-
-
- Passkeys
-
+
+
+
+ Passkeys
+
-
-
- Allows authenticating using biometrics, password managers, hardware keys, etc.
-
-
-
+
+
+ Allows authenticating using biometrics, password managers, hardware keys, etc.
+
+
+
-
-
- Manage passkeys
-
-
-
- )}
+
+
+ Manage passkeys
+
+
+
-
+
View activity
diff --git a/apps/remix/app/routes/_authenticated+/settings+/security+/passkeys+/index.tsx b/apps/remix/app/routes/_authenticated+/settings+/security+/passkeys+/index.tsx
new file mode 100644
index 000000000..53bb2165f
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/settings+/security+/passkeys+/index.tsx
@@ -0,0 +1,31 @@
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react';
+
+import { PasskeyCreateDialog } from '~/components/dialogs/passkey-create-dialog';
+import { SettingsHeader } from '~/components/general/settings-header';
+import { SettingsSecurityPasskeyTable } from '~/components/tables/settings-security-passkey-table';
+import { appMetaTags } from '~/utils/meta';
+
+export function meta() {
+ return appMetaTags('Manage passkeys');
+}
+
+export default function SettingsPasskeys() {
+ const { _ } = useLingui();
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/settings/teams/page.tsx b/apps/remix/app/routes/_authenticated+/settings+/teams+/index.tsx
similarity index 74%
rename from apps/web/src/app/(dashboard)/settings/teams/page.tsx
rename to apps/remix/app/routes/_authenticated+/settings+/teams+/index.tsx
index 439bc9713..ff639fc7c 100644
--- a/apps/web/src/app/(dashboard)/settings/teams/page.tsx
+++ b/apps/remix/app/routes/_authenticated+/settings+/teams+/index.tsx
@@ -1,15 +1,13 @@
-'use client';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { AnimatePresence } from 'framer-motion';
import { trpc } from '@documenso/trpc/react';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { CreateTeamDialog } from '~/components/(teams)/dialogs/create-team-dialog';
-import { UserSettingsTeamsPageDataTable } from '~/components/(teams)/tables/user-settings-teams-page-data-table';
+import { TeamCreateDialog } from '~/components/dialogs/team-create-dialog';
+import { SettingsHeader } from '~/components/general/settings-header';
+import { UserSettingsTeamsPageDataTable } from '~/components/tables/user-settings-teams-page-table';
import { TeamEmailUsage } from './team-email-usage';
import { TeamInvitations } from './team-invitations';
@@ -25,7 +23,7 @@ export default function TeamsSettingsPage() {
title={_(msg`Teams`)}
subtitle={_(msg`Manage all teams you are currently associated with.`)}
>
-
+
diff --git a/apps/web/src/app/(dashboard)/settings/teams/team-email-usage.tsx b/apps/remix/app/routes/_authenticated+/settings+/teams+/team-email-usage.tsx
similarity index 96%
rename from apps/web/src/app/(dashboard)/settings/teams/team-email-usage.tsx
rename to apps/remix/app/routes/_authenticated+/settings+/teams+/team-email-usage.tsx
index 3f7fadf26..5ae8eb1d6 100644
--- a/apps/web/src/app/(dashboard)/settings/teams/team-email-usage.tsx
+++ b/apps/remix/app/routes/_authenticated+/settings+/teams+/team-email-usage.tsx
@@ -1,11 +1,10 @@
-'use client';
-
import { useState } from 'react';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import type { TeamEmail } from '@prisma/client';
-import type { TeamEmail } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
diff --git a/apps/web/src/app/(dashboard)/settings/teams/team-invitations.tsx b/apps/remix/app/routes/_authenticated+/settings+/teams+/team-invitations.tsx
similarity index 64%
rename from apps/web/src/app/(dashboard)/settings/teams/team-invitations.tsx
rename to apps/remix/app/routes/_authenticated+/settings+/teams+/team-invitations.tsx
index 1cef7ea30..60b4c61c8 100644
--- a/apps/web/src/app/(dashboard)/settings/teams/team-invitations.tsx
+++ b/apps/remix/app/routes/_authenticated+/settings+/teams+/team-invitations.tsx
@@ -1,15 +1,16 @@
-'use client';
-
-import { Plural, Trans } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react';
+import { Plural, Trans } from '@lingui/react/macro';
import { AnimatePresence } from 'framer-motion';
import { BellIcon } from 'lucide-react';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { formatTeamUrl } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
+import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
@@ -18,9 +19,7 @@ import {
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
-
-import { AcceptTeamInvitationButton } from './accept-team-invitation-button';
-import { DeclineTeamInvitationButton } from './decline-team-invitation-button';
+import { useToast } from '@documenso/ui/primitives/use-toast';
export const TeamInvitations = () => {
const { data, isLoading } = trpc.team.getTeamInvitations.useQuery();
@@ -83,9 +82,7 @@ export const TeamInvitations = () => {
{data.map((invitation) => (
{
);
};
+
+const AcceptTeamInvitationButton = ({ teamId }: { teamId: number }) => {
+ const { _ } = useLingui();
+ const { toast } = useToast();
+
+ const {
+ mutateAsync: acceptTeamInvitation,
+ isPending,
+ isSuccess,
+ } = trpc.team.acceptTeamInvitation.useMutation({
+ onSuccess: () => {
+ toast({
+ title: _(msg`Success`),
+ description: _(msg`Accepted team invitation`),
+ duration: 5000,
+ });
+ },
+ onError: () => {
+ toast({
+ title: _(msg`Something went wrong`),
+ description: _(msg`Unable to join this team at this time.`),
+ variant: 'destructive',
+ duration: 10000,
+ });
+ },
+ });
+
+ return (
+ acceptTeamInvitation({ teamId })}
+ loading={isPending}
+ disabled={isPending || isSuccess}
+ >
+ Accept
+
+ );
+};
+
+const DeclineTeamInvitationButton = ({ teamId }: { teamId: number }) => {
+ const { _ } = useLingui();
+ const { toast } = useToast();
+
+ const {
+ mutateAsync: declineTeamInvitation,
+ isPending,
+ isSuccess,
+ } = trpc.team.declineTeamInvitation.useMutation({
+ onSuccess: () => {
+ toast({
+ title: _(msg`Success`),
+ description: _(msg`Declined team invitation`),
+ duration: 5000,
+ });
+ },
+ onError: () => {
+ toast({
+ title: _(msg`Something went wrong`),
+ description: _(msg`Unable to decline this team invitation at this time.`),
+ variant: 'destructive',
+ duration: 10000,
+ });
+ },
+ });
+
+ return (
+ declineTeamInvitation({ teamId })}
+ loading={isPending}
+ disabled={isPending || isSuccess}
+ variant="ghost"
+ >
+ Decline
+
+ );
+};
diff --git a/apps/web/src/app/(dashboard)/settings/tokens/page.tsx b/apps/remix/app/routes/_authenticated+/settings+/tokens+/index.tsx
similarity index 75%
rename from apps/web/src/app/(dashboard)/settings/tokens/page.tsx
rename to apps/remix/app/routes/_authenticated+/settings+/tokens+/index.tsx
index ad90b54d5..dcf6bd7d8 100644
--- a/apps/web/src/app/(dashboard)/settings/tokens/page.tsx
+++ b/apps/remix/app/routes/_authenticated+/settings+/tokens+/index.tsx
@@ -1,20 +1,17 @@
-import { Trans } from '@lingui/macro';
+import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { DateTime } from 'luxon';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens';
+import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
-import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
+import TokenDeleteDialog from '~/components/dialogs/token-delete-dialog';
import { ApiTokenForm } from '~/components/forms/token';
-export default async function ApiTokensPage() {
- const { i18n } = await setupI18nSSR();
+export default function ApiTokensPage() {
+ const { i18n } = useLingui();
- const { user } = await getRequiredServerComponentSession();
-
- const tokens = await getUserTokens({ userId: user.id });
+ const { data: tokens } = trpc.apiToken.getTokens.useQuery();
return (
@@ -47,7 +44,7 @@ export default async function ApiTokensPage() {
Your existing tokens
- {tokens.length === 0 && (
+ {tokens && tokens.length === 0 && (
Your tokens will be shown here once you create them.
@@ -55,7 +52,7 @@ export default async function ApiTokensPage() {
)}
- {tokens.length > 0 && (
+ {tokens && tokens.length > 0 && (
{tokens.map((token) => (
@@ -78,11 +75,11 @@ export default async function ApiTokensPage() {
-
+
Delete
-
+
diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx b/apps/remix/app/routes/_authenticated+/settings+/webhooks+/$id.tsx
similarity index 90%
rename from apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx
rename to apps/remix/app/routes/_authenticated+/settings+/webhooks+/$id.tsx
index 3800bdd67..0a77fb274 100644
--- a/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx
+++ b/apps/remix/app/routes/_authenticated+/settings+/webhooks+/$id.tsx
@@ -1,12 +1,10 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form';
+import { useParams, useRevalidator } from 'react-router';
import type { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
@@ -26,29 +24,27 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { Switch } from '@documenso/ui/primitives/switch';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { TriggerMultiSelectCombobox } from '~/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox';
+import { SettingsHeader } from '~/components/general/settings-header';
+import { WebhookMultiSelectCombobox } from '~/components/general/webhook-multiselect-combobox';
const ZEditWebhookFormSchema = ZEditWebhookMutationSchema.omit({ id: true });
type TEditWebhookFormSchema = z.infer;
-export type WebhookPageOptions = {
- params: {
- id: string;
- };
-};
+export default function WebhookPage() {
+ const params = useParams();
-export default function WebhookPage({ params }: WebhookPageOptions) {
const { _ } = useLingui();
const { toast } = useToast();
- const router = useRouter();
+ const { revalidate } = useRevalidator();
+
+ const webhookId = params.id || '';
const { data: webhook, isLoading } = trpc.webhook.getWebhookById.useQuery(
{
- id: params.id,
+ id: webhookId,
},
- { enabled: !!params.id },
+ { enabled: !!webhookId },
);
const { mutateAsync: updateWebhook } = trpc.webhook.editWebhook.useMutation();
@@ -66,7 +62,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
const onSubmit = async (data: TEditWebhookFormSchema) => {
try {
await updateWebhook({
- id: params.id,
+ id: webhookId,
...data,
});
@@ -76,7 +72,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
duration: 5000,
});
- router.refresh();
+ await revalidate();
} catch (err) {
toast({
title: _(msg`Failed to update webhook`),
@@ -163,7 +159,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
Triggers
- {
onChange(values);
diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx b/apps/remix/app/routes/_authenticated+/settings+/webhooks+/index.tsx
similarity index 85%
rename from apps/web/src/app/(dashboard)/settings/webhooks/page.tsx
rename to apps/remix/app/routes/_authenticated+/settings+/webhooks+/index.tsx
index 5da11e6f9..357ce5b3b 100644
--- a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx
+++ b/apps/remix/app/routes/_authenticated+/settings+/webhooks+/index.tsx
@@ -1,11 +1,9 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { DateTime } from 'luxon';
+import { Link } from 'react-router';
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
import { trpc } from '@documenso/trpc/react';
@@ -13,9 +11,9 @@ import { cn } from '@documenso/ui/lib/utils';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
-import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
+import { WebhookCreateDialog } from '~/components/dialogs/webhook-create-dialog';
+import { WebhookDeleteDialog } from '~/components/dialogs/webhook-delete-dialog';
+import { SettingsHeader } from '~/components/general/settings-header';
export default function WebhookPage() {
const { _, i18n } = useLingui();
@@ -28,15 +26,13 @@ export default function WebhookPage() {
title={_(msg`Webhooks`)}
subtitle={_(msg`On this page, you can create new Webhooks and manage the existing ones.`)}
>
-
+
-
{isLoading && (
)}
-
{webhooks && webhooks.length === 0 && (
// TODO: Perhaps add some illustrations here to make the page more engaging
@@ -47,7 +43,6 @@ export default function WebhookPage() {
)}
-
{webhooks && webhooks.length > 0 && (
{webhooks?.map((webhook) => (
@@ -91,15 +86,15 @@ export default function WebhookPage() {
-
+
Edit
-
+
Delete
-
+
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_index.tsx
new file mode 100644
index 000000000..4911167df
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_index.tsx
@@ -0,0 +1,14 @@
+import { redirect } from 'react-router';
+import { getLoaderSession } from 'server/utils/get-loader-session';
+
+import { formatDocumentsPath } from '@documenso/lib/utils/teams';
+
+export function loader() {
+ const { currentTeam } = getLoaderSession();
+
+ if (!currentTeam) {
+ throw redirect('/settings/teams');
+ }
+
+ throw redirect(formatDocumentsPath(currentTeam.url));
+}
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx
new file mode 100644
index 000000000..57159d15f
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx
@@ -0,0 +1,147 @@
+import type { MessageDescriptor } from '@lingui/core';
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { ChevronLeft } from 'lucide-react';
+import { Link, Outlet, isRouteErrorResponse, redirect, useNavigate } from 'react-router';
+import { getLoaderSession } from 'server/utils/get-loader-session';
+import { match } from 'ts-pattern';
+
+import { AppErrorCode } from '@documenso/lib/errors/app-error';
+import { TrpcProvider } from '@documenso/trpc/react';
+import { Button } from '@documenso/ui/primitives/button';
+
+import { TeamProvider } from '~/providers/team';
+
+import type { Route } from './+types/_layout';
+
+export const loader = () => {
+ const { currentTeam } = getLoaderSession();
+
+ if (!currentTeam) {
+ throw redirect('/settings/teams');
+ }
+
+ const trpcHeaders = {
+ 'x-team-Id': currentTeam.id.toString(),
+ };
+
+ return {
+ currentTeam,
+ trpcHeaders,
+ };
+};
+
+export default function Layout({ loaderData }: Route.ComponentProps) {
+ const { currentTeam, trpcHeaders } = loaderData;
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
+ const { _ } = useLingui();
+
+ const navigate = useNavigate();
+
+ let errorMessage = msg`Unknown error`;
+ let errorDetails: MessageDescriptor | null = null;
+
+ if (error instanceof Error && error.message === AppErrorCode.UNAUTHORIZED) {
+ errorMessage = msg`Unauthorized`;
+ errorDetails = msg`You are not authorized to view this page.`;
+ }
+
+ if (isRouteErrorResponse(error)) {
+ return match(error.status)
+ .with(404, () => (
+
+
+
+ 404 Team not found
+
+
+
+ Oops! Something went wrong.
+
+
+
+
+ The team you are looking for may have been removed, renamed or may have never
+ existed.
+
+
+
+
+
+
+
+ Go Back
+
+
+
+
+
+ ))
+ .with(500, () => (
+
+
+
{_(errorMessage)}
+
+
+ Oops! Something went wrong.
+
+
+
+ {errorDetails ? _(errorDetails) : ''}
+
+
+
+ {
+ void navigate(-1);
+ }}
+ >
+
+ Go Back
+
+
+
+
+ View teams
+
+
+
+
+
+ ))
+ .otherwise(() => (
+ <>
+
+ {error.status} {error.statusText}
+
+
{error.data}
+ >
+ ));
+ } else if (error instanceof Error) {
+ return (
+
+
Error
+
{error.message}
+
The stack trace is:
+
{error.stack}
+
+ );
+ } else {
+ return
Unknown Error ;
+ }
+}
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
new file mode 100644
index 000000000..a898bbff3
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@@ -0,0 +1,5 @@
+import DocumentPage, { loader } from '~/routes/_authenticated+/documents+/$id._index';
+
+export { loader };
+
+export default DocumentPage;
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
new file mode 100644
index 000000000..275962cfb
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
@@ -0,0 +1,5 @@
+import DocumentEditPage, { loader } from '~/routes/_authenticated+/documents+/$id.edit';
+
+export { loader };
+
+export default DocumentEditPage;
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx
new file mode 100644
index 000000000..c4dfb7965
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx
@@ -0,0 +1,5 @@
+import DocumentLogsPage, { loader } from '~/routes/_authenticated+/documents+/$id.logs';
+
+export { loader };
+
+export default DocumentLogsPage;
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
new file mode 100644
index 000000000..095d136cf
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
@@ -0,0 +1,5 @@
+import DocumentsPage, { meta } from '~/routes/_authenticated+/documents+/_index';
+
+export { meta };
+
+export default DocumentsPage;
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/page.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings+/_index.tsx
similarity index 78%
rename from apps/web/src/app/(teams)/t/[teamUrl]/settings/page.tsx
rename to apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings+/_index.tsx
index 3a72cb255..367505e89 100644
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/page.tsx
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings+/_index.tsx
@@ -1,40 +1,28 @@
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
import { CheckCircle2, Clock } from 'lucide-react';
import { P, match } from 'ts-pattern';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
+import { useSession } from '@documenso/lib/client-only/providers/session';
+import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { AddTeamEmailDialog } from '~/components/(teams)/dialogs/add-team-email-dialog';
-import { DeleteTeamDialog } from '~/components/(teams)/dialogs/delete-team-dialog';
-import { TransferTeamDialog } from '~/components/(teams)/dialogs/transfer-team-dialog';
-import { UpdateTeamForm } from '~/components/(teams)/forms/update-team-form';
+import { TeamDeleteDialog } from '~/components/dialogs/team-delete-dialog';
+import { TeamEmailAddDialog } from '~/components/dialogs/team-email-add-dialog';
+import { TeamTransferDialog } from '~/components/dialogs/team-transfer-dialog';
import { AvatarImageForm } from '~/components/forms/avatar-image';
+import { TeamUpdateForm } from '~/components/forms/team-update-form';
+import { SettingsHeader } from '~/components/general/settings-header';
+import { TeamEmailDropdown } from '~/components/general/teams/team-email-dropdown';
+import { TeamTransferStatus } from '~/components/general/teams/team-transfer-status';
+import { useCurrentTeam } from '~/providers/team';
-import { TeamEmailDropdown } from './team-email-dropdown';
-import { TeamTransferStatus } from './team-transfer-status';
+export default function TeamsSettingsPage() {
+ const { user } = useSession();
-export type TeamsSettingsPageProps = {
- params: {
- teamUrl: string;
- };
-};
-
-export default async function TeamsSettingsPage({ params }: TeamsSettingsPageProps) {
- await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const session = await getRequiredServerComponentSession();
-
- const team = await getTeamByUrl({ userId: session.user.id, teamUrl });
+ const team = useCurrentTeam();
const isTransferVerificationExpired =
!team.transferVerification || isTokenExpired(team.transferVerification.expiresAt);
@@ -50,9 +38,9 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
transferVerification={team.transferVerification}
/>
-
+
-
+
{(team.teamEmail || team.emailVerification) && (
@@ -73,7 +61,7 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
-
+
)}
- {team.ownerUserId === session.user.id && (
+ {team.ownerUserId === user.id && (
<>
{isTransferVerificationExpired && (
-