diff --git a/.gitignore b/.gitignore index 3b0569b15..b95fcc7d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +packages/prisma/generated/types.ts + # dependencies node_modules .pnp diff --git a/.vscode/settings.json b/.vscode/settings.json index f5542fbb5..e6ff5d1a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,12 +5,7 @@ "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, - "eslint.validate": [ - "typescript", - "typescriptreact", - "javascript", - "javascriptreact" - ], + "eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"], "javascript.preferences.importModuleSpecifier": "non-relative", "javascript.preferences.useAliasesForRenames": false, "typescript.enablePromptUseWorkspaceTsdk": true, @@ -20,4 +15,7 @@ "[prisma]": { "editor.defaultFormatter": "Prisma.prisma" }, -} \ No newline at end of file + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/README.md b/README.md index 74e3bddc5..f32438800 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,7 @@ npm run prisma:migrate-deploy Finally, you can start it with: ``` +cd apps/web npm run start ``` diff --git a/apps/documentation/pages/developers/webhooks.mdx b/apps/documentation/pages/developers/webhooks.mdx index 47cb9a9d6..024f4e493 100644 --- a/apps/documentation/pages/developers/webhooks.mdx +++ b/apps/documentation/pages/developers/webhooks.mdx @@ -37,7 +37,7 @@ To create a new webhook subscription, you need to provide the following informat - Enter the webhook URL that will receive the event payload. - Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`. -- Optionally, you can provide a secret key that will be used to sign the payload. This key will be included in the `X-Documenso-Signature` header of the request. +- Optionally, you can provide a secret key that will be used to sign the payload. This key will be included in the `X-Documenso-Secret` header of the request. ![A screenshot of the Create Webhook modal that shows the URL input field and the event checkboxes](/webhook-images/webhooks-page-create-webhook-modal.webp) diff --git a/apps/marketing/content/blog/announcing-vial-21-cfr-part-11.mdx b/apps/marketing/content/blog/announcing-vial-21-cfr-part-11.mdx index 540f7a0a5..b64953c8c 100644 --- a/apps/marketing/content/blog/announcing-vial-21-cfr-part-11.mdx +++ b/apps/marketing/content/blog/announcing-vial-21-cfr-part-11.mdx @@ -11,14 +11,7 @@ tags: - Compliance --- - +
Vial.com uses Documenso for 21 CFR Part 11 compliant signing.
@@ -26,42 +19,40 @@ tags: > TLDR; We launched Vial.com on Documenso and are open for 21 CFR Part 11 business. # What is 21 CFR -You have never heard of 21 CFR Part 11? You are in good company since most people haven't. If you have, you probably work in an industry regulated by the U.S. Food and Drug Administration (FDA). Title 21 of the Code of Federal Regulations (CFR) is dedicated to detailing FDA-regulated business, and sub-part 11 sets out guidelines for using electronic signatures in this highly regulated field. Hence, 21 CFR Part 11 is highly relevant for regulated industries that aim to employ digital signatures. The guidelines set out in 21 CFR Part 11 aim to provide trustworthy, reliable, and equivalent to paper records and handwritten signatures. All Industries that fall under the FDA's regulation, e.g. pharmaceuticals, biotechnology, medical devices, and biologics, must comply with these rules when choosing or creating systems for electronic signatures. + +You have never heard of 21 CFR Part 11? You are in good company since most people haven't. If you have, you probably work in an industry regulated by the U.S. Food and Drug Administration (FDA). Title 21 of the Code of Federal Regulations (CFR) is dedicated to detailing FDA-regulated business, and sub-part 11 sets out guidelines for using electronic signatures in this highly regulated field. Hence, 21 CFR Part 11 is highly relevant for regulated industries that aim to employ digital signatures. The guidelines set out in 21 CFR Part 11 aim to provide trustworthy, reliable, and equivalent to paper records and handwritten signatures. All Industries that fall under the FDA's regulation, e.g. pharmaceuticals, biotechnology, medical devices, and biologics, must comply with these rules when choosing or creating systems for electronic signatures. Compliance with 21 CFR Part 11 is crucial for companies to use electronic records and signatures in their operations legally. It affects how companies manage documentation, conduct audits, and maintain regulatory submissions. Non-compliance can result in legal penalties, rejected submissions, and delays in product approvals, emphasizing the importance of adhering to these guidelines in FDA-regulated activities. # Vial.com + Vial is a technology company on a mission to advance programs to market through computationally designed therapeutics and cost-effective clinical trials. It is imperative that Vial manages this process securely, effectively, and highly compliant. By leveraging it's modern platform, Vial aims to accelerate drug development and, ultimately, time to market for new therapies. You can learn more about them [here](https://vial.com/about-us). [Together](https://documen.so/vial-documenso), Documenso and Vial set out to create the first open-source, 21 CFR Part 11 compliant signing solution. After iterating over the product together, Vial moved their operation from DocuSign, a known legacy signing provider, to a Documenso Enterprise plan. We are very happy to be able to support Vial’s mission by fulfilling our own: bringing open signing and all its innovation to where it's needed. # 21 CFR Part 11 on Documenso Highlights + 21 CFR Part 11 is a highly complex statute, and going into the all design rationales and the following implementation details, deserves its own article later. For now, I want to share a few notable highlights. ## The Full Experience + We implemented 21 CFR Part 11, keeping the main user experience of Documenso intact. Our 21 CFR module is not separate but natively integrated into all Documenso flows, thus not sacrificing usability for compliance. This also means most (if not all) advanced features we offer are usable in a compliant way. This prevents customers from being trapped in an anti-innovation bubble, not allowing access to new features for fear of non-compliance. ## Action Reauth Using Passkeys - + +
Using passkeys (used here via fingerprint scanner) is the smoothest way to re-authenticate.
- One of the requirements affecting day-to-day life the most is the requirement to actually reauthenticate every signature placed on a document. While we can't change that, we can help make the reauthentication as painless as possible. To this end, we opted for passkeys. While Documenso supports passkeys to log in, they are also supported to authenticate signing on a per-signature level as part of the Documenso Enterprise Plan. The user still has to authenticate every signature but can now do so from the comfort of their passkey provider, be that 1Password, their browser, or any other provider. ## Direct Links + We recently launched [Direct Template Links](https://documen.so/direct-links), a new way to let people sign and fill out forms. Links can be completed anytime, creating a new document in the process. Direct Links are also 21 CFR part 11 compliant, using action reauthentication, audit log, and all other compliance requirements. # Documenso Enterprise Plan + With the successful launch of Vial, we are now open for business. 21 CFR Part 11 compliance is part of the Documenso Enterprise plan, which includes all regulations we currently support and upcoming additions. While the pricing depends heavily on your needs and scale, we offer fixed-price plans for better predictability for both sides. In our experience, volume-based pricing is a legacy headache we want to avoid. If you are FDA-regulated and looking for a modern signing solution, we are happy to discuss your requirements in detail. You can write us (hi@documenso.com) or contact [our enterprise team](https://documen.so/21cfr) at any time or stage. @@ -70,4 +61,3 @@ If you have any questions or comments, please reach out on [Twitter / X](https:/ Best from Hamburg\ Timur - diff --git a/apps/marketing/package.json b/apps/marketing/package.json index fb6478fca..3b122d0f4 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -1,6 +1,6 @@ { "name": "@documenso/marketing", - "version": "1.6.1-rc.1", + "version": "1.7.0-rc.2", "private": true, "license": "AGPL-3.0", "scripts": { @@ -20,7 +20,6 @@ "@documenso/trpc": "*", "@documenso/ui": "*", "@hookform/resolvers": "^3.1.0", - "@lingui/macro": "^4.11.1", "@lingui/react": "^4.11.1", "@openstatus/react": "^0.0.3", "cmdk": "^0.2.1", diff --git a/apps/marketing/src/app/(marketing)/oss-friends/page.tsx b/apps/marketing/src/app/(marketing)/oss-friends/page.tsx index 65a4a55f8..78b051eea 100644 --- a/apps/marketing/src/app/(marketing)/oss-friends/page.tsx +++ b/apps/marketing/src/app/(marketing)/oss-friends/page.tsx @@ -44,6 +44,10 @@ export default async function OSSFriendsPage() { src={backgroundPattern} alt="background pattern" className="-mr-[15vw] -mt-[15vh] h-full max-h-[150vh] scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:-mr-[50vw] md:scale-150 lg:scale-[175%]" + style={{ + mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)', + WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)', + }} /> diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx index 2d8bbe9d9..a3df5af29 100644 --- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx +++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx @@ -5,6 +5,8 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import { msg } from '@lingui/macro'; + import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { base64 } from '@documenso/lib/universal/base64'; @@ -46,8 +48,8 @@ export const SinglePlayerClient = () => { const documentFlow: Record = { fields: { - title: 'Add document', - description: 'Upload a document and add fields.', + title: msg`Add document`, + description: msg`Upload a document and add fields.`, stepIndex: 1, onBackStep: uploadedFile ? () => { @@ -58,8 +60,8 @@ export const SinglePlayerClient = () => { onNextStep: () => setStep('sign'), }, sign: { - title: 'Sign', - description: 'Enter your details.', + title: msg`Sign`, + description: msg`Enter your details.`, stepIndex: 2, onBackStep: () => setStep('fields'), }, diff --git a/apps/marketing/src/app/not-found.tsx b/apps/marketing/src/app/not-found.tsx index d85cdb62f..044f7f6c7 100644 --- a/apps/marketing/src/app/not-found.tsx +++ b/apps/marketing/src/app/not-found.tsx @@ -26,6 +26,10 @@ export default function NotFound() { src={backgroundPattern} alt="background pattern" className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-100 lg:scale-[100%]" + style={{ + mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)', + WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)', + }} priority /> diff --git a/apps/marketing/src/components/(marketing)/carousel.tsx b/apps/marketing/src/components/(marketing)/carousel.tsx index 688f79b3a..07b089d33 100644 --- a/apps/marketing/src/components/(marketing)/carousel.tsx +++ b/apps/marketing/src/components/(marketing)/carousel.tsx @@ -2,11 +2,14 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; +import Link from 'next/link'; + import { Trans, msg } from '@lingui/macro'; import { useLingui } from '@lingui/react'; import type { AutoplayType } from 'embla-carousel-autoplay'; import Autoplay from 'embla-carousel-autoplay'; import useEmblaCarousel from 'embla-carousel-react'; +import { usePlausible } from 'next-plausible'; import { useTheme } from 'next-themes'; import { Card } from '@documenso/ui/primitives/card'; @@ -61,6 +64,7 @@ const SLIDES = [ export const Carousel = () => { const { _ } = useLingui(); + const event = usePlausible(); const slides = SLIDES; const [_isPlaying, setIsPlaying] = useState(false); @@ -238,7 +242,10 @@ export const Carousel = () => { if (!mounted) return null; return ( <> - +
{slides.map((slide, index) => ( @@ -269,6 +276,19 @@ export const Carousel = () => {
+ + event('view-demo')} + > + Book a Demo + + Want to learn more about Documenso and how it works? Book a demo today! Our founders + will walk you through the application and answer any questions you may have regarding + usage, integration, and more. + +
diff --git a/apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx b/apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx index 998f9ac75..6578a61ed 100644 --- a/apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx +++ b/apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx @@ -24,6 +24,10 @@ export const FasterSmarterBeautifulBento = ({ src={backgroundPattern} alt="background pattern" className="h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]" + style={{ + mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)', + WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)', + }} />

diff --git a/apps/marketing/src/components/(marketing)/hero.tsx b/apps/marketing/src/components/(marketing)/hero.tsx index bc5818399..11f6ac790 100644 --- a/apps/marketing/src/components/(marketing)/hero.tsx +++ b/apps/marketing/src/components/(marketing)/hero.tsx @@ -86,6 +86,10 @@ export const Hero = ({ className, ...props }: HeroProps) => { src={backgroundPattern} alt="background pattern" className="-mr-[50vw] -mt-[15vh] h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]" + style={{ + mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)', + WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)', + }} />

diff --git a/apps/marketing/src/components/(marketing)/open-build-template-bento.tsx b/apps/marketing/src/components/(marketing)/open-build-template-bento.tsx index a0708716a..a0b99c0f0 100644 --- a/apps/marketing/src/components/(marketing)/open-build-template-bento.tsx +++ b/apps/marketing/src/components/(marketing)/open-build-template-bento.tsx @@ -21,6 +21,10 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat src={backgroundPattern} alt="background pattern" className="h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]" + style={{ + mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)', + WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)', + }} />

diff --git a/apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx b/apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx index 418e2f712..b7b07c62c 100644 --- a/apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx +++ b/apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx @@ -25,6 +25,10 @@ export const ShareConnectPaidWidgetBento = ({ src={backgroundPattern} alt="background pattern" className="h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]" + style={{ + mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)', + WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)', + }} />

@@ -39,7 +43,7 @@ export const ShareConnectPaidWidgetBento = ({

- Easy Sharing (Soon). + Easy Sharing. Receive your personal link to share with everyone you care about.

diff --git a/apps/web/lingui.config.ts b/apps/web/lingui.config.ts index f129b49ce..a5862eb98 100644 --- a/apps/web/lingui.config.ts +++ b/apps/web/lingui.config.ts @@ -8,7 +8,7 @@ const config: LinguiConfig = { locales: APP_I18N_OPTIONS.supportedLangs as unknown as string[], catalogs: [ { - path: '/../../packages/lib/translations/web/{locale}', + path: '/../../packages/lib/translations/{locale}/web', include: ['/apps/web/src'], }, { diff --git a/apps/web/package.json b/apps/web/package.json index f86e5769a..47d7ceda6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@documenso/web", - "version": "1.6.1-rc.1", + "version": "1.7.0-rc.2", "private": true, "license": "AGPL-3.0", "scripts": { @@ -23,7 +23,6 @@ "@documenso/trpc": "*", "@documenso/ui": "*", "@hookform/resolvers": "^3.1.0", - "@lingui/macro": "^4.11.1", "@lingui/react": "^4.11.1", "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.3", @@ -60,9 +59,9 @@ "zod": "^3.22.4" }, "devDependencies": { + "@documenso/tailwind-config": "*", "@lingui/loader": "^4.11.1", "@lingui/swc-plugin": "4.0.6", - "@documenso/tailwind-config": "*", "@simplewebauthn/types": "^9.0.1", "@types/formidable": "^2.0.6", "@types/luxon": "^3.3.1", diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx index f084b5db5..330f31eb1 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx @@ -2,6 +2,9 @@ import Link from 'next/link'; +import { Trans, msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; + import type { Recipient } from '@documenso/prisma/client'; import { type Document, SigningStatus } from '@documenso/prisma/client'; import { trpc } from '@documenso/trpc/react'; @@ -22,20 +25,21 @@ export type AdminActionsProps = { }; export const AdminActions = ({ className, document, recipients }: AdminActionsProps) => { + const { _ } = useLingui(); const { toast } = useToast(); const { mutate: resealDocument, isLoading: isResealDocumentLoading } = trpc.admin.resealDocument.useMutation({ onSuccess: () => { toast({ - title: 'Success', - description: 'Document resealed', + title: _(msg`Success`), + description: _(msg`Document resealed`), }); }, onError: () => { toast({ - title: 'Error', - description: 'Failed to reseal document', + title: _(msg`Error`), + description: _(msg`Failed to reseal document`), variant: 'destructive', }); }, @@ -54,19 +58,23 @@ export const AdminActions = ({ className, document, recipients }: AdminActionsPr )} onClick={() => resealDocument({ id: document.id })} > - Reseal document + Reseal document - Attempts sealing the document again, useful for after a code change has occurred to - resolve an erroneous document. + + Attempts sealing the document again, useful for after a code change has occurred to + resolve an erroneous document. + ); diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx index 563db8d1b..48211fbc0 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx @@ -1,5 +1,7 @@ +import { Trans } from '@lingui/macro'; import { DateTime } from 'luxon'; +import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; import { Accordion, @@ -23,6 +25,8 @@ type AdminDocumentDetailsPageProps = { }; export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) { + setupI18nSSR(); + const document = await getEntireDocument({ id: Number(params.id) }); return ( @@ -35,28 +39,34 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument {document.deletedAt && ( - Deleted + Deleted )}
- Created on: + Created on:{' '} +
- Last updated at: + Last updated at:{' '} +

-

Admin Actions

+

+ Admin Actions +


-

Recipients

+

+ Recipients +

diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx index 3bf8c78ab..c4864145c 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx @@ -1,7 +1,11 @@ 'use client'; +import { useMemo } from 'react'; + import { useRouter } from 'next/navigation'; +import { Trans, msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -13,6 +17,7 @@ import { } 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'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { Form, @@ -43,7 +48,9 @@ export type RecipientItemProps = { }; export const RecipientItem = ({ recipient }: RecipientItemProps) => { + const { _ } = useLingui(); const { toast } = useToast(); + const router = useRouter(); const form = useForm({ @@ -55,6 +62,50 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => { const { mutateAsync: updateRecipient } = trpc.admin.updateRecipient.useMutation(); + const columns = useMemo(() => { + return [ + { + header: 'ID', + accessorKey: 'id', + cell: ({ row }) =>
{row.original.id}
, + }, + { + header: _(msg`Type`), + accessorKey: 'type', + cell: ({ row }) =>
{row.original.type}
, + }, + { + header: _(msg`Inserted`), + accessorKey: 'inserted', + cell: ({ row }) =>
{row.original.inserted ? 'True' : 'False'}
, + }, + { + header: _(msg`Value`), + accessorKey: 'customText', + cell: ({ row }) =>
{row.original.customText}
, + }, + { + header: _(msg`Signature`), + accessorKey: 'signature', + cell: ({ row }) => ( +
+ {row.original.Signature?.typedSignature && ( + {row.original.Signature.typedSignature} + )} + + {row.original.Signature?.signatureImageAsBase64 && ( + Signature + )} +
+ ), + }, + ] satisfies DataTableColumnDef<(typeof recipient)['Field'][number]>[]; + }, []); + const onUpdateRecipientFormSubmit = async ({ name, email }: TAdminUpdateRecipientFormSchema) => { try { await updateRecipient({ @@ -64,14 +115,14 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => { }); toast({ - title: 'Recipient updated', - description: 'The recipient has been updated successfully', + title: _(msg`Recipient updated`), + description: _(msg`The recipient has been updated successfully`), }); router.refresh(); } catch (error) { toast({ - title: 'Failed to update recipient', + title: _(msg`Failed to update recipient`), description: error.message, variant: 'destructive', }); @@ -93,7 +144,9 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => { name="name" render={({ field }) => ( - Name + + Name + @@ -109,7 +162,9 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => { name="email" render={({ field }) => ( - Email + + Email + @@ -122,7 +177,7 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
@@ -131,52 +186,11 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
-

Fields

+

+ Fields +

-
{row.original.id}
, - }, - { - header: 'Type', - accessorKey: 'type', - cell: ({ row }) =>
{row.original.type}
, - }, - { - header: 'Inserted', - accessorKey: 'inserted', - cell: ({ row }) =>
{row.original.inserted ? 'True' : 'False'}
, - }, - { - header: 'Value', - accessorKey: 'customText', - cell: ({ row }) =>
{row.original.customText}
, - }, - { - header: 'Signature', - accessorKey: 'signature', - cell: ({ row }) => ( -
- {row.original.Signature?.typedSignature && ( - {row.original.Signature.typedSignature} - )} - - {row.original.Signature?.signatureImageAsBase64 && ( - Signature - )} -
- ), - }, - ]} - /> +
); }; diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx index 63ad88a3f..337796959 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx @@ -4,6 +4,9 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; +import { Trans, msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; + import type { Document } from '@documenso/prisma/client'; import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; @@ -26,7 +29,9 @@ export type SuperDeleteDocumentDialogProps = { }; export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialogProps) => { + const { _ } = useLingui(); const { toast } = useToast(); + const router = useRouter(); const [reason, setReason] = useState(''); @@ -43,7 +48,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo await deleteDocument({ id: document.id, reason }); toast({ - title: 'Document deleted', + title: _(msg`Document deleted`), description: 'The Document has been deleted successfully.', duration: 5000, }); @@ -52,13 +57,13 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo } catch (err) { if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { toast({ - title: 'An error occurred', + title: _(msg`An error occurred`), description: err.message, variant: 'destructive', }); } else { toast({ - title: 'An unknown error occurred', + title: _(msg`An unknown error occurred`), variant: 'destructive', description: err.message ?? @@ -76,31 +81,41 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo variant="neutral" >
- Delete Document + + Delete Document + - Delete the document. This action is irreversible so proceed with caution. + + Delete the document. This action is irreversible so proceed with caution. +
- + - Delete Document + + Delete Document + - This action is not reversible. Please be certain. + This action is not reversible. Please be certain.
- To confirm, please enter the reason + + To confirm, please enter the reason + - {isDeletingDocument ? 'Deleting document...' : 'Delete Document'} + Delete document diff --git a/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx b/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx index b7e235981..1686e0d41 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx @@ -1,10 +1,12 @@ 'use client'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; import { Loader } from 'lucide-react'; import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; @@ -12,6 +14,7 @@ import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-upda import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { trpc } from '@documenso/trpc/react'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; +import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; import { Input } from '@documenso/ui/primitives/input'; @@ -23,6 +26,8 @@ import { LocaleDate } from '~/components/formatter/locale-date'; // export type AdminDocumentResultsProps = {}; export const AdminDocumentResults = () => { + const { _ } = useLingui(); + const searchParams = useSearchParams(); const updateSearchParams = useUpdateSearchParams(); @@ -45,6 +50,83 @@ export const AdminDocumentResults = () => { }, ); + const results = findDocumentsData ?? { + data: [], + perPage: 20, + currentPage: 1, + totalPages: 1, + }; + + const columns = useMemo(() => { + return [ + { + header: _(msg`Created`), + accessorKey: 'createdAt', + cell: ({ row }) => , + }, + { + header: _(msg`Title`), + accessorKey: 'title', + cell: ({ row }) => { + return ( + + {row.original.title} + + ); + }, + }, + { + header: _(msg`Status`), + accessorKey: 'status', + cell: ({ row }) => , + }, + { + header: _(msg`Owner`), + accessorKey: 'owner', + cell: ({ row }) => { + const avatarFallbackText = row.original.User.name + ? extractInitials(row.original.User.name) + : row.original.User.email.slice(0, 1).toUpperCase(); + + return ( + + + + + + {avatarFallbackText} + + + + + + + + + {avatarFallbackText} + + + +
+ {row.original.User.name} + {row.original.User.email} +
+
+
+ ); + }, + }, + { + header: 'Last updated', + accessorKey: 'updatedAt', + cell: ({ row }) => , + }, + ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[]; + }, []); + const onPaginationChange = (newPage: number, newPerPage: number) => { updateSearchParams({ page: newPage, @@ -56,84 +138,18 @@ export const AdminDocumentResults = () => {
setTerm(e.target.value)} />
, - }, - { - header: 'Title', - accessorKey: 'title', - cell: ({ row }) => { - return ( - - {row.original.title} - - ); - }, - }, - { - header: 'Status', - accessorKey: 'status', - cell: ({ row }) => , - }, - { - header: 'Owner', - accessorKey: 'owner', - cell: ({ row }) => { - const avatarFallbackText = row.original.User.name - ? extractInitials(row.original.User.name) - : row.original.User.email.slice(0, 1).toUpperCase(); - - return ( - - - - - - {avatarFallbackText} - - - - - - - - - {avatarFallbackText} - - - -
- {row.original.User.name} - {row.original.User.email} -
-
-
- ); - }, - }, - { - header: 'Last updated', - accessorKey: 'updatedAt', - cell: ({ row }) => , - }, - ]} - data={findDocumentsData?.data ?? []} - perPage={findDocumentsData?.perPage ?? 20} - currentPage={findDocumentsData?.currentPage ?? 1} - totalPages={findDocumentsData?.totalPages ?? 1} + columns={columns} + data={results.data} + perPage={results.perPage ?? 20} + currentPage={results.currentPage ?? 1} + totalPages={results.totalPages ?? 1} onPaginationChange={onPaginationChange} > {(table) => } diff --git a/apps/web/src/app/(dashboard)/admin/documents/page.tsx b/apps/web/src/app/(dashboard)/admin/documents/page.tsx index 96e4dcef8..7f21bf5fa 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/page.tsx @@ -1,9 +1,17 @@ +import { Trans } from '@lingui/macro'; + +import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; + import { AdminDocumentResults } from './document-results'; export default function AdminDocumentsPage() { + setupI18nSSR(); + return (
-

Manage documents

+

+ Manage documents +

diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index 12330679d..c489c34a1 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { redirect } from 'next/navigation'; +import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; @@ -12,6 +13,8 @@ export type AdminSectionLayoutProps = { }; export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) { + setupI18nSSR(); + const { user } = await getRequiredServerComponentSession(); if (!isAdmin(user)) { diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx index 080cb9741..cf0bb81f2 100644 --- a/apps/web/src/app/(dashboard)/admin/nav.tsx +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -5,6 +5,7 @@ import type { HTMLAttributes } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { Trans } from '@lingui/macro'; import { BarChart3, FileStack, Settings, Users, Wallet2 } from 'lucide-react'; import { cn } from '@documenso/ui/lib/utils'; @@ -33,7 +34,7 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { > - Stats + Stats @@ -47,7 +48,7 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { > - Users + Users @@ -61,7 +62,7 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { > - Documents + Documents @@ -75,7 +76,7 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { > - Subscriptions + Subscriptions @@ -89,7 +90,7 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { > - Site Settings + Site Settings
diff --git a/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx b/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx index 351e146ff..d68eed63b 100644 --- a/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx +++ b/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx @@ -3,6 +3,8 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; +import { Trans, msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; import { useForm } from 'react-hook-form'; import type { z } from 'zod'; @@ -37,8 +39,10 @@ export type BannerFormProps = { }; export function BannerForm({ banner }: BannerFormProps) { - const router = useRouter(); const { toast } = useToast(); + const { _ } = useLingui(); + + const router = useRouter(); const form = useForm({ resolver: zodResolver(ZBannerFormSchema), @@ -67,8 +71,8 @@ export function BannerForm({ banner }: BannerFormProps) { }); toast({ - title: 'Banner Updated', - description: 'Your banner has been updated successfully.', + title: _(msg`Banner Updated`), + description: _(msg`Your banner has been updated successfully.`), duration: 5000, }); @@ -76,16 +80,17 @@ export function BannerForm({ banner }: BannerFormProps) { } catch (err) { if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { toast({ - title: 'An error occurred', + title: _(msg`An error occurred`), description: err.message, variant: 'destructive', }); } else { toast({ - title: 'An unknown error occurred', + title: _(msg`An unknown error occurred`), variant: 'destructive', - description: - 'We encountered an unknown error while attempting to update the banner. Please try again later.', + description: _( + msg`We encountered an unknown error while attempting to update the banner. Please try again later.`, + ), }); } } @@ -93,10 +98,14 @@ export function BannerForm({ banner }: BannerFormProps) { return (
-

Site Banner

+

+ 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. + + 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. +

@@ -110,7 +119,9 @@ export function BannerForm({ banner }: BannerFormProps) { name="enabled" render={({ field }) => ( - Enabled + + Enabled +
@@ -131,7 +142,9 @@ export function BannerForm({ banner }: BannerFormProps) { name="data.bgColor" render={({ field }) => ( - Background Color + + Background Color +
@@ -149,7 +162,9 @@ export function BannerForm({ banner }: BannerFormProps) { name="data.textColor" render={({ field }) => ( - Text Color + + Text Color +
@@ -170,14 +185,16 @@ export function BannerForm({ banner }: BannerFormProps) { name="data.content" render={({ field }) => ( - Content + + Content +