Merge branch 'feat/refresh' into fix/whitespace
This commit is contained in:
7
.eslintignore
Normal file
7
.eslintignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Config files
|
||||||
|
*.config.js
|
||||||
|
*.config.cjs
|
||||||
|
|
||||||
|
# Statically hosted javascript files
|
||||||
|
apps/*/public/*.js
|
||||||
|
apps/*/public/*.cjs
|
||||||
@@ -5,7 +5,7 @@ import { allDocuments } from 'contentlayer/generated';
|
|||||||
import type { MDXComponents } from 'mdx/types';
|
import type { MDXComponents } from 'mdx/types';
|
||||||
import { useMDXComponent } from 'next-contentlayer/hooks';
|
import { useMDXComponent } from 'next-contentlayer/hooks';
|
||||||
|
|
||||||
export const generateStaticParams = async () =>
|
export const generateStaticParams = () =>
|
||||||
allDocuments.map((post) => ({ post: post._raw.flattenedPath }));
|
allDocuments.map((post) => ({ post: post._raw.flattenedPath }));
|
||||||
|
|
||||||
export const generateMetadata = ({ params }: { params: { content: string } }) => {
|
export const generateMetadata = ({ params }: { params: { content: string } }) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ChevronLeft } from 'lucide-react';
|
|||||||
import type { MDXComponents } from 'mdx/types';
|
import type { MDXComponents } from 'mdx/types';
|
||||||
import { useMDXComponent } from 'next-contentlayer/hooks';
|
import { useMDXComponent } from 'next-contentlayer/hooks';
|
||||||
|
|
||||||
export const generateStaticParams = async () =>
|
export const generateStaticParams = () =>
|
||||||
allBlogPosts.map((post) => ({ post: post._raw.flattenedPath }));
|
allBlogPosts.map((post) => ({ post: post._raw.flattenedPath }));
|
||||||
|
|
||||||
export const generateMetadata = ({ params }: { params: { post: string } }) => {
|
export const generateMetadata = ({ params }: { params: { post: string } }) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { FundingRaised } from './funding-raised';
|
|||||||
import { GithubMetric } from './gh-metrics';
|
import { GithubMetric } from './gh-metrics';
|
||||||
import { TeamMembers } from './team-members';
|
import { TeamMembers } from './team-members';
|
||||||
|
|
||||||
export const revalidate = 86400;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
const ZGithubStatsResponse = z.object({
|
const ZGithubStatsResponse = z.object({
|
||||||
stargazers_count: z.number(),
|
stargazers_count: z.number(),
|
||||||
@@ -43,7 +43,7 @@ export default async function OpenPage() {
|
|||||||
accept: 'application/vnd.github.v3+json',
|
accept: 'application/vnd.github.v3+json',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.then((res) => ZGithubStatsResponse.parse(res));
|
.then((res) => ZGithubStatsResponse.parse(res));
|
||||||
|
|
||||||
const { total_count: mergedPullRequests } = await fetch(
|
const { total_count: mergedPullRequests } = await fetch(
|
||||||
@@ -54,7 +54,7 @@ export default async function OpenPage() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then((res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.then((res) => ZMergedPullRequestsResponse.parse(res));
|
.then((res) => ZMergedPullRequestsResponse.parse(res));
|
||||||
|
|
||||||
const STARGAZERS_DATA = await fetch('https://stargrazer-live.onrender.com/api/stats', {
|
const STARGAZERS_DATA = await fetch('https://stargrazer-live.onrender.com/api/stats', {
|
||||||
@@ -62,7 +62,7 @@ export default async function OpenPage() {
|
|||||||
accept: 'application/json',
|
accept: 'application/json',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.then((res) => ZStargazersLiveResponse.parse(res));
|
.then((res) => ZStargazersLiveResponse.parse(res));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default async function IndexPage() {
|
|||||||
accept: 'application/vnd.github.v3+json',
|
accept: 'application/vnd.github.v3+json',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.then((res) => (typeof res.stargazers_count === 'number' ? res.stargazers_count : undefined))
|
.then((res) => (typeof res.stargazers_count === 'number' ? res.stargazers_count : undefined))
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
|||||||
|
|
||||||
const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => {
|
const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => {
|
||||||
try {
|
try {
|
||||||
const delay = new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
const delay = new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
const [redirectUrl] = await Promise.all([
|
const [redirectUrl] = await Promise.all([
|
||||||
claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }),
|
claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const PasswordReveal = ({ password }: PasswordRevealProps) => {
|
|||||||
const [, copy] = useCopyToClipboard();
|
const [, copy] = useCopyToClipboard();
|
||||||
|
|
||||||
const onCopyClick = () => {
|
const onCopyClick = () => {
|
||||||
copy(password).then(() => {
|
void copy(password).then(() => {
|
||||||
toast({
|
toast({
|
||||||
title: 'Copied to clipboard',
|
title: 'Copied to clipboard',
|
||||||
description: 'Your password has been copied to your clipboard.',
|
description: 'Your password has been copied to your clipboard.',
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
setValue('signatureDataUrl', draftSignatureDataUrl);
|
setValue('signatureDataUrl', draftSignatureDataUrl);
|
||||||
setValue('signatureText', '');
|
setValue('signatureText', '');
|
||||||
|
|
||||||
trigger('signatureDataUrl');
|
void trigger('signatureDataUrl');
|
||||||
setShowSigningDialog(false);
|
setShowSigningDialog(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +135,9 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
signatureText,
|
signatureText,
|
||||||
}: TWidgetFormSchema) => {
|
}: TWidgetFormSchema) => {
|
||||||
try {
|
try {
|
||||||
const delay = new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
const delay = new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||||
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
|
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.214.0",
|
"lucide-react": "^0.214.0",
|
||||||
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"next": "13.4.12",
|
"next": "13.4.12",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/formidable": "^2.0.6",
|
"@types/formidable": "^2.0.6",
|
||||||
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/node": "20.1.0",
|
"@types/node": "20.1.0",
|
||||||
"@types/react": "18.2.18",
|
"@types/react": "18.2.18",
|
||||||
"@types/react-dom": "18.2.7"
|
"@types/react-dom": "18.2.7"
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ import { LocaleDate } from '~/components/formatter/locale-date';
|
|||||||
import { UploadDocument } from './upload-document';
|
import { UploadDocument } from './upload-document';
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const session = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const [stats, results] = await Promise.all([
|
const [stats, results] = await Promise.all([
|
||||||
getStats({
|
getStats({
|
||||||
userId: session.id,
|
user,
|
||||||
}),
|
}),
|
||||||
findDocuments({
|
findDocuments({
|
||||||
userId: session.id,
|
userId: user.id,
|
||||||
perPage: 10,
|
perPage: 10,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { Edit, Pencil, Share } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
export type DataTableActionButtonProps = {
|
||||||
|
row: Document & {
|
||||||
|
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
|
Recipient: Recipient[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
|
||||||
|
|
||||||
|
const isOwner = row.User.id === session.user.id;
|
||||||
|
const isRecipient = !!recipient;
|
||||||
|
const isDraft = row.status === DocumentStatus.DRAFT;
|
||||||
|
const isPending = row.status === DocumentStatus.PENDING;
|
||||||
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
|
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
|
|
||||||
|
return match({
|
||||||
|
isOwner,
|
||||||
|
isRecipient,
|
||||||
|
isDraft,
|
||||||
|
isPending,
|
||||||
|
isComplete,
|
||||||
|
isSigned,
|
||||||
|
})
|
||||||
|
.with({ isOwner: true, isDraft: true }, () => (
|
||||||
|
<Button className="w-24" asChild>
|
||||||
|
<Link href={`/documents/${row.id}`}>
|
||||||
|
<Edit className="-ml-1 mr-2 h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
|
||||||
|
<Button className="w-24" asChild>
|
||||||
|
<Link href={`/sign/${recipient?.token}`}>
|
||||||
|
<Pencil className="-ml-1 mr-2 h-4 w-4" />
|
||||||
|
Sign
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
.otherwise(() => (
|
||||||
|
<Button className="w-24" disabled>
|
||||||
|
<Share className="-ml-1 mr-2 h-4 w-4" />
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
));
|
||||||
|
};
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
Download,
|
||||||
|
Edit,
|
||||||
|
History,
|
||||||
|
MoreHorizontal,
|
||||||
|
Pencil,
|
||||||
|
Share,
|
||||||
|
Trash2,
|
||||||
|
XCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
|
import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
|
|
||||||
|
export type DataTableActionDropdownProps = {
|
||||||
|
row: Document & {
|
||||||
|
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
|
Recipient: Recipient[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
|
||||||
|
|
||||||
|
const isOwner = row.User.id === session.user.id;
|
||||||
|
// const isRecipient = !!recipient;
|
||||||
|
// const isDraft = row.status === DocumentStatus.DRAFT;
|
||||||
|
// const isPending = row.status === DocumentStatus.PENDING;
|
||||||
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
|
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
|
|
||||||
|
const onDownloadClick = () => {
|
||||||
|
let decodedDocument = row.document;
|
||||||
|
|
||||||
|
try {
|
||||||
|
decodedDocument = atob(decodedDocument);
|
||||||
|
} catch (err) {
|
||||||
|
// We're just going to ignore this error and try to download the document
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentBytes = Uint8Array.from(decodedDocument.split('').map((c) => c.charCodeAt(0)));
|
||||||
|
|
||||||
|
const blob = new Blob([documentBytes], {
|
||||||
|
type: 'application/pdf',
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = window.document.createElement('a');
|
||||||
|
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = row.title || 'document.pdf';
|
||||||
|
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
window.URL.revokeObjectURL(link.href);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<MoreHorizontal className="h-5 w-5 text-gray-500" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
|
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||||
|
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled={!recipient} asChild>
|
||||||
|
<Link href={`/sign/${recipient?.token}`}>
|
||||||
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
|
Sign
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled={!isOwner} asChild>
|
||||||
|
<Link href={`/documents/${row.id}`}>
|
||||||
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled={!isComplete} onClick={onDownloadClick}>
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<XCircle className="mr-2 h-4 w-4" />
|
||||||
|
Void
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuLabel>Share</DropdownMenuLabel>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<History className="mr-2 h-4 w-4" />
|
||||||
|
Resend
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Share className="mr-2 h-4 w-4" />
|
||||||
|
Share
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ import { Loader } from 'lucide-react';
|
|||||||
|
|
||||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||||
import { FindResultSet } from '@documenso/lib/types/find-result-set';
|
import { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||||
import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient';
|
import { Document, Recipient, User } from '@documenso/prisma/client';
|
||||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||||
|
|
||||||
@@ -16,8 +16,16 @@ import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-a
|
|||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||||
|
|
||||||
|
import { DataTableActionButton } from './data-table-action-button';
|
||||||
|
import { DataTableActionDropdown } from './data-table-action-dropdown';
|
||||||
|
|
||||||
export type DocumentsDataTableProps = {
|
export type DocumentsDataTableProps = {
|
||||||
results: FindResultSet<DocumentWithReciepient>;
|
results: FindResultSet<
|
||||||
|
Document & {
|
||||||
|
Recipient: Recipient[];
|
||||||
|
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
||||||
@@ -45,7 +53,11 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
|||||||
{
|
{
|
||||||
header: 'Title',
|
header: 'Title',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Link href={`/documents/${row.original.id}`} className="font-medium hover:underline">
|
<Link
|
||||||
|
href={`/documents/${row.original.id}`}
|
||||||
|
title={row.original.title}
|
||||||
|
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
|
||||||
|
>
|
||||||
{row.original.title}
|
{row.original.title}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
@@ -67,6 +79,15 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
|||||||
accessorKey: 'created',
|
accessorKey: 'created',
|
||||||
cell: ({ row }) => <LocaleDate date={row.getValue('created')} />,
|
cell: ({ row }) => <LocaleDate date={row.getValue('created')} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'Actions',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-x-4">
|
||||||
|
<DataTableActionButton row={row.original} />
|
||||||
|
<DataTableActionDropdown row={row.original} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
data={results.data}
|
data={results.data}
|
||||||
perPage={results.perPage}
|
perPage={results.perPage}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import Link from 'next/link';
|
|||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
||||||
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
||||||
import { isDocumentStatus } from '@documenso/lib/types/is-document-status';
|
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
||||||
@@ -16,7 +16,7 @@ import { DocumentsDataTable } from './data-table';
|
|||||||
|
|
||||||
export type DocumentsPageProps = {
|
export type DocumentsPageProps = {
|
||||||
searchParams?: {
|
searchParams?: {
|
||||||
status?: InternalDocumentStatus | 'ALL';
|
status?: ExtendedDocumentStatus;
|
||||||
period?: PeriodSelectorValue;
|
period?: PeriodSelectorValue;
|
||||||
page?: string;
|
page?: string;
|
||||||
perPage?: string;
|
perPage?: string;
|
||||||
@@ -24,22 +24,20 @@ export type DocumentsPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||||
const session = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const stats = await getStats({
|
const stats = await getStats({
|
||||||
userId: session.id,
|
user,
|
||||||
});
|
});
|
||||||
|
|
||||||
const status = isDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
|
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
|
||||||
// const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
|
// const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
|
||||||
const page = Number(searchParams.page) || 1;
|
const page = Number(searchParams.page) || 1;
|
||||||
const perPage = Number(searchParams.perPage) || 20;
|
const perPage = Number(searchParams.perPage) || 20;
|
||||||
|
|
||||||
const shouldDefaultToPending = status === 'ALL' && stats.PENDING > 0;
|
|
||||||
|
|
||||||
const results = await findDocuments({
|
const results = await findDocuments({
|
||||||
userId: session.id,
|
userId: user.id,
|
||||||
status: status === 'ALL' ? undefined : status,
|
status,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
column: 'created',
|
column: 'created',
|
||||||
direction: 'desc',
|
direction: 'desc',
|
||||||
@@ -57,10 +55,6 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
params.delete('page');
|
params.delete('page');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === 'ALL') {
|
|
||||||
params.delete('status');
|
|
||||||
}
|
|
||||||
|
|
||||||
return `/documents?${params.toString()}`;
|
return `/documents?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,41 +65,27 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
<h1 className="mt-12 text-4xl font-semibold">Documents</h1>
|
<h1 className="mt-12 text-4xl font-semibold">Documents</h1>
|
||||||
|
|
||||||
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
|
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
|
||||||
<Tabs defaultValue={shouldDefaultToPending ? InternalDocumentStatus.PENDING : status}>
|
<Tabs defaultValue={status} className="overflow-x-auto">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.PENDING} asChild>
|
{[
|
||||||
<Link href={getTabHref(InternalDocumentStatus.PENDING)}>
|
ExtendedDocumentStatus.INBOX,
|
||||||
<DocumentStatus status={InternalDocumentStatus.PENDING} />
|
ExtendedDocumentStatus.PENDING,
|
||||||
|
ExtendedDocumentStatus.COMPLETED,
|
||||||
|
ExtendedDocumentStatus.DRAFT,
|
||||||
|
ExtendedDocumentStatus.ALL,
|
||||||
|
].map((value) => (
|
||||||
|
<TabsTrigger key={value} className="min-w-[60px]" value={value} asChild>
|
||||||
|
<Link href={getTabHref(value)} scroll={false}>
|
||||||
|
<DocumentStatus status={value} />
|
||||||
|
|
||||||
|
{value !== ExtendedDocumentStatus.ALL && (
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
<span className="ml-1 hidden opacity-50 md:inline-block">
|
||||||
{Math.min(stats.PENDING, 99)}
|
{Math.min(stats[value], 99)}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.COMPLETED} asChild>
|
|
||||||
<Link href={getTabHref(InternalDocumentStatus.COMPLETED)}>
|
|
||||||
<DocumentStatus status={InternalDocumentStatus.COMPLETED} />
|
|
||||||
|
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
|
||||||
{Math.min(stats.COMPLETED, 99)}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.DRAFT} asChild>
|
|
||||||
<Link href={getTabHref(InternalDocumentStatus.DRAFT)}>
|
|
||||||
<DocumentStatus status={InternalDocumentStatus.DRAFT} />
|
|
||||||
|
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
|
||||||
{Math.min(stats.DRAFT, 99)}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger className="min-w-[60px]" value="ALL" asChild>
|
|
||||||
<Link href={getTabHref('ALL')}>All</Link>
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => {
|
|||||||
disabled={!localFullName}
|
disabled={!localFullName}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowFullNameModal(false);
|
setShowFullNameModal(false);
|
||||||
onSign('local');
|
void onSign('local');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Sign
|
Sign
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
|
|||||||
disabled={!localSignature}
|
disabled={!localSignature}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowSignatureModal(false);
|
setShowSignatureModal(false);
|
||||||
onSign('local');
|
void onSign('local');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Sign
|
Sign
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import { cn } from '@documenso/ui/lib/utils';
|
|||||||
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||||
|
// const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('ml-8 hidden flex-1 gap-x-6 md:flex', className)} {...props}>
|
<div className={cn('ml-8 hidden flex-1 gap-x-6 md:flex', className)} {...props}>
|
||||||
{/* No Nav tabs while there is only one main page */}
|
{/* We have no other subpaths rn */}
|
||||||
{/* <Link
|
{/* <Link
|
||||||
href="/documents"
|
href="/documents"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ import { HTMLAttributes } from 'react';
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Menu } from 'lucide-react';
|
|
||||||
|
|
||||||
import { User } from '@documenso/prisma/client';
|
import { User } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { Logo } from '~/components/branding/logo';
|
||||||
|
|
||||||
@@ -23,7 +20,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
|
|||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-40 flex h-16 w-full items-center border-b backdrop-blur',
|
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-50 flex h-16 w-full items-center border-b backdrop-blur',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -41,9 +38,9 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
|
|||||||
<div className="flex gap-x-4">
|
<div className="flex gap-x-4">
|
||||||
<ProfileDropdown user={user} />
|
<ProfileDropdown user={user} />
|
||||||
|
|
||||||
<Button variant="outline" size="sm" className="h-10 w-10 p-0.5 md:hidden">
|
{/* <Button variant="outline" size="sm" className="h-10 w-10 p-0.5 md:hidden">
|
||||||
<Menu className="h-6 w-6" />
|
<Menu className="h-6 w-6" />
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
|
|||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
signOut({
|
void signOut({
|
||||||
callbackUrl: '/',
|
callbackUrl: '/',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const PeriodSelector = () => {
|
|||||||
params.delete('period');
|
params.delete('period');
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push(`${pathname}?${params.toString()}`);
|
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
|||||||
|
|
||||||
const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => {
|
const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => {
|
||||||
try {
|
try {
|
||||||
const delay = new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
const delay = new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
const [redirectUrl] = await Promise.all([
|
const [redirectUrl] = await Promise.all([
|
||||||
claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }),
|
claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const PasswordReveal = ({ password }: PasswordRevealProps) => {
|
|||||||
const [, copy] = useCopyToClipboard();
|
const [, copy] = useCopyToClipboard();
|
||||||
|
|
||||||
const onCopyClick = () => {
|
const onCopyClick = () => {
|
||||||
copy(password).then(() => {
|
void copy(password).then(() => {
|
||||||
toast({
|
toast({
|
||||||
title: 'Copied to clipboard',
|
title: 'Copied to clipboard',
|
||||||
description: 'Your password has been copied to your clipboard.',
|
description: 'Your password has been copied to your clipboard.',
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
setValue('signatureDataUrl', draftSignatureDataUrl);
|
setValue('signatureDataUrl', draftSignatureDataUrl);
|
||||||
setValue('signatureText', '');
|
setValue('signatureText', '');
|
||||||
|
|
||||||
trigger('signatureDataUrl');
|
void trigger('signatureDataUrl');
|
||||||
setShowSigningDialog(false);
|
setShowSigningDialog(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +135,9 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
signatureText,
|
signatureText,
|
||||||
}: TWidgetFormSchema) => {
|
}: TWidgetFormSchema) => {
|
||||||
try {
|
try {
|
||||||
const delay = new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
const delay = new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||||
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
|
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ import { HTMLAttributes } from 'react';
|
|||||||
import { CheckCircle2, Clock, File } from 'lucide-react';
|
import { CheckCircle2, Clock, File } from 'lucide-react';
|
||||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||||
|
|
||||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
type FriendlyStatus = {
|
type FriendlyStatus = {
|
||||||
label: string;
|
label: string;
|
||||||
icon: LucideIcon;
|
icon?: LucideIcon;
|
||||||
color: string;
|
color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus> = {
|
||||||
PENDING: {
|
PENDING: {
|
||||||
label: 'Pending',
|
label: 'Pending',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
@@ -28,10 +29,19 @@ const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
|||||||
icon: File,
|
icon: File,
|
||||||
color: 'text-yellow-500',
|
color: 'text-yellow-500',
|
||||||
},
|
},
|
||||||
|
INBOX: {
|
||||||
|
label: 'Inbox',
|
||||||
|
icon: SignatureIcon,
|
||||||
|
color: 'text-muted-foreground',
|
||||||
|
},
|
||||||
|
ALL: {
|
||||||
|
label: 'All',
|
||||||
|
color: 'text-muted-foreground',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
||||||
status: InternalDocumentStatus;
|
status: ExtendedDocumentStatus;
|
||||||
inheritColor?: boolean;
|
inheritColor?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,11 +55,13 @@ export const DocumentStatus = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn('flex items-center', className)} {...props}>
|
<span className={cn('flex items-center', className)} {...props}>
|
||||||
|
{Icon && (
|
||||||
<Icon
|
<Icon
|
||||||
className={cn('mr-2 inline-block h-4 w-4', {
|
className={cn('mr-2 inline-block h-4 w-4', {
|
||||||
[color]: !inheritColor,
|
[color]: !inheritColor,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -76,10 +76,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className={cn('flex w-full flex-col gap-y-4', className)}
|
className={cn('flex w-full flex-col gap-y-4', className)}
|
||||||
onSubmit={(e) => {
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
e.preventDefault();
|
|
||||||
handleSubmit(onFormSubmit)();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="email" className="text-slate-500">
|
<Label htmlFor="email" className="text-slate-500">
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const getFlag = async (
|
|||||||
revalidate: 60,
|
revalidate: 60,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.then((res) => ZFeatureFlagValueSchema.parse(res))
|
.then((res) => ZFeatureFlagValueSchema.parse(res))
|
||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export const getAllFlags = async (
|
|||||||
revalidate: 60,
|
revalidate: 60,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
|
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
|
||||||
.catch(() => LOCAL_FEATURE_FLAGS);
|
.catch(() => LOCAL_FEATURE_FLAGS);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export default function PostHogServerClient() {
|
|||||||
|
|
||||||
return new PostHog(postHogConfig.key, {
|
return new PostHog(postHogConfig.key, {
|
||||||
host: postHogConfig.host,
|
host: postHogConfig.host,
|
||||||
fetch: (...args) => fetch(...args),
|
fetch: async (...args) => fetch(...args),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
18
apps/web/src/hooks/use-debounced-value.ts
Normal file
18
apps/web/src/hooks/use-debounced-value.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useDebouncedValue<T>(value: T, delay: number) {
|
||||||
|
// State and setters for debounced value
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
export default async function middleware(req: NextRequest) {
|
export default function middleware(req: NextRequest) {
|
||||||
if (req.nextUrl.pathname === '/') {
|
if (req.nextUrl.pathname === '/') {
|
||||||
const redirectUrl = new URL('/documents', req.url);
|
const redirectUrl = new URL('/documents', req.url);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { appRouter } from '@documenso/trpc/server/router';
|
|||||||
|
|
||||||
export default trpcNext.createNextApiHandler({
|
export default trpcNext.createNextApiHandler({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: ({ req, res }) => createTrpcContext({ req, res }),
|
createContext: async ({ req, res }) => createTrpcContext({ req, res }),
|
||||||
});
|
});
|
||||||
|
|
||||||
// export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
|
// export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function FeatureFlagProvider({
|
|||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (document.hasFocus()) {
|
if (document.hasFocus()) {
|
||||||
getAllFlags().then((newFlags) => setFlags(newFlags));
|
void getAllFlags().then((newFlags) => setFlags(newFlags));
|
||||||
}
|
}
|
||||||
}, FEATURE_FLAG_POLL_INTERVAL);
|
}, FEATURE_FLAG_POLL_INTERVAL);
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ export function FeatureFlagProvider({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFocus = () => getAllFlags().then((newFlags) => setFlags(newFlags));
|
const onFocus = () => void getAllFlags().then((newFlags) => setFlags(newFlags));
|
||||||
|
|
||||||
window.addEventListener('focus', onFocus);
|
window.addEventListener('focus', onFocus);
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ module.exports = {
|
|||||||
content: [
|
content: [
|
||||||
...baseConfig.content,
|
...baseConfig.content,
|
||||||
`${path.join(require.resolve('@documenso/ui'), '..')}/**/*.{ts,tsx}`,
|
`${path.join(require.resolve('@documenso/ui'), '..')}/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/email'), '..')}/**/*.{ts,tsx}`,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -79,6 +79,7 @@
|
|||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.214.0",
|
"lucide-react": "^0.214.0",
|
||||||
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"next": "13.4.12",
|
"next": "13.4.12",
|
||||||
@@ -101,6 +102,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/formidable": "^2.0.6",
|
"@types/formidable": "^2.0.6",
|
||||||
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/node": "20.1.0",
|
"@types/node": "20.1.0",
|
||||||
"@types/react": "18.2.18",
|
"@types/react": "18.2.18",
|
||||||
"@types/react-dom": "18.2.7"
|
"@types/react-dom": "18.2.7"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"turbo": "^1.9.3"
|
"turbo": "^1.9.3"
|
||||||
},
|
},
|
||||||
"name": "documenso.next",
|
"name": "@documenso/root",
|
||||||
"packageManager": "npm@8.19.2",
|
"packageManager": "npm@8.19.2",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
export interface TemplateDocumentCompletedProps {
|
||||||
|
downloadLink: string;
|
||||||
|
reviewLink: string;
|
||||||
|
documentName: string;
|
||||||
|
assetBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateDocumentCompleted = ({
|
||||||
|
downloadLink,
|
||||||
|
reviewLink,
|
||||||
|
documentName,
|
||||||
|
assetBaseUrl,
|
||||||
|
}: TemplateDocumentCompletedProps) => {
|
||||||
|
const getAssetUrl = (path: string) => {
|
||||||
|
return new URL(path, assetBaseUrl).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: config.theme.extend.colors,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Section className="flex-row items-center justify-center">
|
||||||
|
<div className="flex items-center justify-center p-4">
|
||||||
|
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
|
||||||
|
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||||
|
Completed
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||||
|
“{documentName}” was signed by all signers
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
|
Continue by downloading or reviewing the document.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section className="mb-6 mt-8 text-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||||
|
href={reviewLink}
|
||||||
|
>
|
||||||
|
<Img src={getAssetUrl('/static/review.png')} className="-mb-1 mr-2 inline h-5 w-5" />
|
||||||
|
Review
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||||
|
href={downloadLink}
|
||||||
|
>
|
||||||
|
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
</Tailwind>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateDocumentCompleted;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
export interface TemplateDocumentInviteProps {
|
||||||
|
inviterName: string;
|
||||||
|
inviterEmail: string;
|
||||||
|
documentName: string;
|
||||||
|
signDocumentLink: string;
|
||||||
|
assetBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateDocumentInvite = ({
|
||||||
|
inviterName,
|
||||||
|
documentName,
|
||||||
|
signDocumentLink,
|
||||||
|
assetBaseUrl,
|
||||||
|
}: TemplateDocumentInviteProps) => {
|
||||||
|
const getAssetUrl = (path: string) => {
|
||||||
|
return new URL(path, assetBaseUrl).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: config.theme.extend.colors,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Section className="mt-4 flex-row items-center justify-center">
|
||||||
|
<div className="flex items-center justify-center p-4">
|
||||||
|
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
|
{inviterName} has invited you to sign "{documentName}"
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
|
Continue by signing the document.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section className="mb-6 mt-8 text-center">
|
||||||
|
<Button
|
||||||
|
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
|
||||||
|
href={signDocumentLink}
|
||||||
|
>
|
||||||
|
Sign Document
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
</Tailwind>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateDocumentInvite;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Img, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
export interface TemplateDocumentPendingProps {
|
||||||
|
documentName: string;
|
||||||
|
assetBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateDocumentPending = ({
|
||||||
|
documentName,
|
||||||
|
assetBaseUrl,
|
||||||
|
}: TemplateDocumentPendingProps) => {
|
||||||
|
const getAssetUrl = (path: string) => {
|
||||||
|
return new URL(path, assetBaseUrl).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: config.theme.extend.colors,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Section className="flex-row items-center justify-center">
|
||||||
|
<div className="flex items-center justify-center p-4">
|
||||||
|
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500">
|
||||||
|
<Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||||
|
Waiting for others
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||||
|
“{documentName}” has been signed
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
|
||||||
|
We're still waiting for other signers to sign this document.
|
||||||
|
<br />
|
||||||
|
We'll notify you as soon as it's ready.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Tailwind>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateDocumentPending;
|
||||||
22
packages/email/template-components/template-footer.tsx
Normal file
22
packages/email/template-components/template-footer.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Link, Section, Text } from '@react-email/components';
|
||||||
|
|
||||||
|
export const TemplateFooter = () => {
|
||||||
|
return (
|
||||||
|
<Section>
|
||||||
|
<Text className="my-4 text-base text-slate-400">
|
||||||
|
This document was sent using{' '}
|
||||||
|
<Link className="text-[#7AC455]" href="https://documenso.com">
|
||||||
|
Documenso.
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="my-8 text-sm text-slate-400">
|
||||||
|
Documenso
|
||||||
|
<br />
|
||||||
|
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateFooter;
|
||||||
@@ -1,25 +1,23 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Button,
|
|
||||||
Container,
|
Container,
|
||||||
Head,
|
Head,
|
||||||
Html,
|
Html,
|
||||||
Img,
|
Img,
|
||||||
Link,
|
|
||||||
Preview,
|
Preview,
|
||||||
Section,
|
Section,
|
||||||
Tailwind,
|
Tailwind,
|
||||||
Text,
|
|
||||||
} from '@react-email/components';
|
} from '@react-email/components';
|
||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
interface DocumentCompletedEmailTemplateProps {
|
import {
|
||||||
downloadLink?: string;
|
TemplateDocumentCompleted,
|
||||||
reviewLink?: string;
|
TemplateDocumentCompletedProps,
|
||||||
documentName?: string;
|
} from '../template-components/template-document-completed';
|
||||||
assetBaseUrl?: string;
|
import TemplateFooter from '../template-components/template-footer';
|
||||||
}
|
|
||||||
|
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;
|
||||||
|
|
||||||
export const DocumentCompletedEmailTemplate = ({
|
export const DocumentCompletedEmailTemplate = ({
|
||||||
downloadLink = 'https://documenso.com',
|
downloadLink = 'https://documenso.com',
|
||||||
@@ -50,74 +48,23 @@ export const DocumentCompletedEmailTemplate = ({
|
|||||||
<Section className="bg-white">
|
<Section className="bg-white">
|
||||||
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
|
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
|
||||||
<Section className="p-2">
|
<Section className="p-2">
|
||||||
<Img src={getAssetUrl('/static/logo.png')} alt="Documenso Logo" className="h-6" />
|
|
||||||
|
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
|
||||||
<div className="flex items-center justify-center p-4">
|
|
||||||
<Img
|
<Img
|
||||||
className="h-42"
|
src={getAssetUrl('/static/logo.png')}
|
||||||
src={getAssetUrl('/static/document.png')}
|
alt="Documenso Logo"
|
||||||
alt="Documenso"
|
className="mb-4 h-6"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
|
<TemplateDocumentCompleted
|
||||||
<Img
|
downloadLink={downloadLink}
|
||||||
src={getAssetUrl('/static/completed.png')}
|
reviewLink={reviewLink}
|
||||||
className="-mb-0.5 mr-2 inline h-7 w-7"
|
documentName={documentName}
|
||||||
|
assetBaseUrl={assetBaseUrl}
|
||||||
/>
|
/>
|
||||||
Completed
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
|
||||||
“{documentName}” was signed by all signers
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
|
||||||
Continue by downloading or reviewing the document.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Section className="mb-6 mt-8 text-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
|
||||||
href={reviewLink}
|
|
||||||
>
|
|
||||||
<Img
|
|
||||||
src={getAssetUrl('/static/review.png')}
|
|
||||||
className="-mb-1 mr-2 inline h-5 w-5"
|
|
||||||
/>
|
|
||||||
Review
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
|
||||||
href={downloadLink}
|
|
||||||
>
|
|
||||||
<Img
|
|
||||||
src={getAssetUrl('/static/download.png')}
|
|
||||||
className="-mb-1 mr-2 inline h-5 w-5"
|
|
||||||
/>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Container className="mx-auto max-w-xl">
|
<Container className="mx-auto max-w-xl">
|
||||||
<Section>
|
<TemplateFooter />
|
||||||
<Text className="my-4 text-base text-slate-400">
|
|
||||||
This document was sent using{' '}
|
|
||||||
<Link className="text-[#7AC455]" href="https://documenso.com">
|
|
||||||
Documenso.
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="my-8 text-sm text-slate-400">
|
|
||||||
Documenso
|
|
||||||
<br />
|
|
||||||
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Section>
|
</Section>
|
||||||
</Body>
|
</Body>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Button,
|
|
||||||
Container,
|
Container,
|
||||||
Head,
|
Head,
|
||||||
Hr,
|
Hr,
|
||||||
@@ -15,13 +14,13 @@ import {
|
|||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
interface DocumentInviteEmailTemplateProps {
|
import {
|
||||||
inviterName?: string;
|
TemplateDocumentInvite,
|
||||||
inviterEmail?: string;
|
TemplateDocumentInviteProps,
|
||||||
documentName?: string;
|
} from '../template-components/template-document-invite';
|
||||||
signDocumentLink?: string;
|
import TemplateFooter from '../template-components/template-footer';
|
||||||
assetBaseUrl?: string;
|
|
||||||
}
|
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps>;
|
||||||
|
|
||||||
export const DocumentInviteEmailTemplate = ({
|
export const DocumentInviteEmailTemplate = ({
|
||||||
inviterName = 'Lucas Smith',
|
inviterName = 'Lucas Smith',
|
||||||
@@ -51,36 +50,21 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
>
|
>
|
||||||
<Body className="mx-auto my-auto bg-white font-sans">
|
<Body className="mx-auto my-auto bg-white font-sans">
|
||||||
<Section>
|
<Section>
|
||||||
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
|
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
|
||||||
<Section className="p-2">
|
<Section>
|
||||||
<Img src={getAssetUrl('/static/logo.png')} alt="Documenso Logo" className="h-6" />
|
|
||||||
|
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
|
||||||
<div className="flex items-center justify-center p-4">
|
|
||||||
<Img
|
<Img
|
||||||
className="h-42"
|
src={getAssetUrl('/static/logo.png')}
|
||||||
src={getAssetUrl('/static/document.png')}
|
alt="Documenso Logo"
|
||||||
alt="Documenso"
|
className="mb-4 h-6"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
<TemplateDocumentInvite
|
||||||
{inviterName} has invited you to sign "{documentName}"
|
inviterName={inviterName}
|
||||||
</Text>
|
inviterEmail={inviterEmail}
|
||||||
|
documentName={documentName}
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
signDocumentLink={signDocumentLink}
|
||||||
Continue by signing the document.
|
assetBaseUrl={assetBaseUrl}
|
||||||
</Text>
|
/>
|
||||||
|
|
||||||
<Section className="mb-6 mt-8 text-center">
|
|
||||||
<Button
|
|
||||||
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
|
|
||||||
href={signDocumentLink}
|
|
||||||
>
|
|
||||||
Sign Document
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
@@ -102,20 +86,7 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
<Hr className="mx-auto mt-12 max-w-xl" />
|
<Hr className="mx-auto mt-12 max-w-xl" />
|
||||||
|
|
||||||
<Container className="mx-auto max-w-xl">
|
<Container className="mx-auto max-w-xl">
|
||||||
<Section>
|
<TemplateFooter />
|
||||||
<Text className="my-4 text-base text-slate-400">
|
|
||||||
This document was sent using{' '}
|
|
||||||
<Link className="text-[#7AC455]" href="https://documenso.com">
|
|
||||||
Documenso.
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="my-8 text-sm text-slate-400">
|
|
||||||
Documenso
|
|
||||||
<br />
|
|
||||||
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Section>
|
</Section>
|
||||||
</Body>
|
</Body>
|
||||||
|
|||||||
@@ -4,19 +4,20 @@ import {
|
|||||||
Head,
|
Head,
|
||||||
Html,
|
Html,
|
||||||
Img,
|
Img,
|
||||||
Link,
|
|
||||||
Preview,
|
Preview,
|
||||||
Section,
|
Section,
|
||||||
Tailwind,
|
Tailwind,
|
||||||
Text,
|
|
||||||
} from '@react-email/components';
|
} from '@react-email/components';
|
||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
interface DocumentPendingEmailTemplateProps {
|
import {
|
||||||
documentName?: string;
|
TemplateDocumentPending,
|
||||||
assetBaseUrl?: string;
|
TemplateDocumentPendingProps,
|
||||||
}
|
} from '../template-components/template-document-pending';
|
||||||
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
|
|
||||||
|
export type DocumentPendingEmailTemplateProps = Partial<TemplateDocumentPendingProps>;
|
||||||
|
|
||||||
export const DocumentPendingEmailTemplate = ({
|
export const DocumentPendingEmailTemplate = ({
|
||||||
documentName = 'Open Source Pledge.pdf',
|
documentName = 'Open Source Pledge.pdf',
|
||||||
@@ -43,55 +44,20 @@ export const DocumentPendingEmailTemplate = ({
|
|||||||
>
|
>
|
||||||
<Body className="mx-auto my-auto font-sans">
|
<Body className="mx-auto my-auto font-sans">
|
||||||
<Section className="bg-white">
|
<Section className="bg-white">
|
||||||
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
|
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
|
||||||
<Section className="p-2">
|
<Section>
|
||||||
<Img src={getAssetUrl('/static/logo.png')} alt="Documenso Logo" className="h-6" />
|
|
||||||
|
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
|
||||||
<div className="flex items-center justify-center p-4">
|
|
||||||
<Img
|
<Img
|
||||||
className="h-42"
|
src={getAssetUrl('/static/logo.png')}
|
||||||
src={getAssetUrl('/static/document.png')}
|
alt="Documenso Logo"
|
||||||
alt="Documenso"
|
className="mb-4 h-6"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500">
|
<TemplateDocumentPending documentName={documentName} assetBaseUrl={assetBaseUrl} />
|
||||||
<Img
|
|
||||||
src={getAssetUrl('/static/clock.png')}
|
|
||||||
className="-mb-0.5 mr-2 inline h-7 w-7"
|
|
||||||
/>
|
|
||||||
Waiting for others
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
|
||||||
“{documentName}” has been signed
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
|
|
||||||
We're still waiting for other signers to sign this document.
|
|
||||||
<br />
|
|
||||||
We'll notify you as soon as it's ready.
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Container className="mx-auto max-w-xl">
|
<Container className="mx-auto max-w-xl">
|
||||||
<Section>
|
<TemplateFooter />
|
||||||
<Text className="my-4 text-base text-slate-400">
|
|
||||||
This document was sent using{' '}
|
|
||||||
<Link className="text-[#7AC455]" href="https://documenso.com">
|
|
||||||
Documenso.
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="my-8 text-sm text-slate-400">
|
|
||||||
Documenso
|
|
||||||
<br />
|
|
||||||
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Section>
|
</Section>
|
||||||
</Body>
|
</Body>
|
||||||
|
|||||||
@@ -110,9 +110,10 @@ export class MailChannelsTransport implements Transport<SentMessageInfo> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json().then((data) => {
|
res
|
||||||
return callback(new Error(`MailChannels error: ${data.message}`), null);
|
.json()
|
||||||
});
|
.then((data) => callback(new Error(`MailChannels error: ${data.message}`), null))
|
||||||
|
.catch((err) => callback(err, null));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
return callback(err, null);
|
return callback(err, null);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ module.exports = {
|
|||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ['../../apps/*/tsconfig.json', '../../packages/*/tsconfig.json'],
|
||||||
ecmaVersion: 2022,
|
ecmaVersion: 2022,
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true,
|
jsx: true,
|
||||||
@@ -33,6 +35,18 @@ module.exports = {
|
|||||||
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||||
|
|
||||||
|
// Safety with promises so we aren't running with scissors
|
||||||
|
'no-promise-executor-return': 'error',
|
||||||
|
'prefer-promise-reject-errors': 'error',
|
||||||
|
'require-atomic-updates': 'error',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
'@typescript-eslint/no-misused-promises': [
|
||||||
|
'error',
|
||||||
|
{ checksVoidReturn: { attributes: false } },
|
||||||
|
],
|
||||||
|
'@typescript-eslint/promise-function-async': 'error',
|
||||||
|
'@typescript-eslint/require-await': 'error',
|
||||||
|
|
||||||
// We never want to use `as` but are required to on occasion to handle
|
// We never want to use `as` but are required to on occasion to handle
|
||||||
// shortcomings in third-party and generated types.
|
// shortcomings in third-party and generated types.
|
||||||
//
|
//
|
||||||
|
|||||||
9
packages/eslint-config/tsconfig.json
Normal file
9
packages/eslint-config/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@documenso/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"noEmit": true,
|
||||||
|
},
|
||||||
|
"include": ["**/*.cjs", "**/*.js"],
|
||||||
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
}
|
||||||
@@ -89,7 +89,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async session({ token, session }) {
|
session({ token, session }) {
|
||||||
if (token && token.email) {
|
if (token && token.email) {
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next';
|
import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { headers } from 'next/headers';
|
|
||||||
import { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
import { getServerSession as getNextAuthServerSession } from 'next-auth';
|
import { getServerSession as getNextAuthServerSession } from 'next-auth';
|
||||||
import { getToken } from 'next-auth/jwt';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
@@ -30,18 +27,6 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
|
|||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerComponentToken = async () => {
|
|
||||||
const requestHeaders = Object.fromEntries(headers().entries());
|
|
||||||
|
|
||||||
const req = new NextRequest('http://example.com', {
|
|
||||||
headers: requestHeaders,
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = await getToken({
|
|
||||||
req,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getServerComponentSession = async () => {
|
export const getServerComponentSession = async () => {
|
||||||
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
|
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,6 @@ export const completeDocumentWithToken = async ({
|
|||||||
|
|
||||||
if (documents.count > 0) {
|
if (documents.count > 0) {
|
||||||
console.log('sealing document');
|
console.log('sealing document');
|
||||||
sealDocument({ documentId: document.id });
|
await sealDocument({ documentId: document.id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { Document, DocumentStatus, Prisma } from '@documenso/prisma/client';
|
import { Document, Prisma, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
|
||||||
import { FindResultSet } from '../../types/find-result-set';
|
import { FindResultSet } from '../../types/find-result-set';
|
||||||
|
|
||||||
export interface FindDocumentsOptions {
|
export interface FindDocumentsOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
term?: string;
|
term?: string;
|
||||||
status?: DocumentStatus;
|
status?: ExtendedDocumentStatus;
|
||||||
page?: number;
|
page?: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
orderBy?: {
|
orderBy?: {
|
||||||
@@ -19,29 +21,102 @@ export interface FindDocumentsOptions {
|
|||||||
export const findDocuments = async ({
|
export const findDocuments = async ({
|
||||||
userId,
|
userId,
|
||||||
term,
|
term,
|
||||||
status,
|
status = ExtendedDocumentStatus.ALL,
|
||||||
page = 1,
|
page = 1,
|
||||||
perPage = 10,
|
perPage = 10,
|
||||||
orderBy,
|
orderBy,
|
||||||
}: FindDocumentsOptions): Promise<FindResultSet<DocumentWithReciepient>> => {
|
}: FindDocumentsOptions) => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const orderByColumn = orderBy?.column ?? 'created';
|
const orderByColumn = orderBy?.column ?? 'created';
|
||||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||||
|
|
||||||
const filters: Prisma.DocumentWhereInput = {
|
const termFilters = !term
|
||||||
status,
|
? undefined
|
||||||
userId,
|
: ({
|
||||||
};
|
title: {
|
||||||
|
|
||||||
if (term) {
|
|
||||||
filters.title = {
|
|
||||||
contains: term,
|
contains: term,
|
||||||
mode: 'insensitive',
|
mode: 'insensitive',
|
||||||
};
|
},
|
||||||
}
|
} as const);
|
||||||
|
|
||||||
|
const filters = match<ExtendedDocumentStatus, Prisma.DocumentWhereInput>(status)
|
||||||
|
.with(ExtendedDocumentStatus.ALL, () => ({
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: {
|
||||||
|
not: ExtendedDocumentStatus.DRAFT,
|
||||||
|
},
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
.with(ExtendedDocumentStatus.INBOX, () => ({
|
||||||
|
status: {
|
||||||
|
not: ExtendedDocumentStatus.DRAFT,
|
||||||
|
},
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
signingStatus: SigningStatus.NOT_SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.with(ExtendedDocumentStatus.DRAFT, () => ({
|
||||||
|
userId,
|
||||||
|
status: ExtendedDocumentStatus.DRAFT,
|
||||||
|
}))
|
||||||
|
.with(ExtendedDocumentStatus.PENDING, () => ({
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
status: ExtendedDocumentStatus.PENDING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: ExtendedDocumentStatus.PENDING,
|
||||||
|
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
.with(ExtendedDocumentStatus.COMPLETED, () => ({
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
status: ExtendedDocumentStatus.COMPLETED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: ExtendedDocumentStatus.COMPLETED,
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
.exhaustive();
|
||||||
|
|
||||||
const [data, count] = await Promise.all([
|
const [data, count] = await Promise.all([
|
||||||
prisma.document.findMany({
|
prisma.document.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
...termFilters,
|
||||||
...filters,
|
...filters,
|
||||||
},
|
},
|
||||||
skip: Math.max(page - 1, 0) * perPage,
|
skip: Math.max(page - 1, 0) * perPage,
|
||||||
@@ -50,21 +125,37 @@ export const findDocuments = async ({
|
|||||||
[orderByColumn]: orderByDirection,
|
[orderByColumn]: orderByDirection,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
User: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.document.count({
|
prisma.document.count({
|
||||||
where: {
|
where: {
|
||||||
|
...termFilters,
|
||||||
...filters,
|
...filters,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const maskedData = data.map((doc) => ({
|
||||||
|
...doc,
|
||||||
|
Recipient: doc.Recipient.map((recipient) => ({
|
||||||
|
...recipient,
|
||||||
|
token: recipient.email === user.email ? recipient.token : '',
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data: maskedData,
|
||||||
count,
|
count,
|
||||||
currentPage: Math.max(page, 1),
|
currentPage: Math.max(page, 1),
|
||||||
perPage,
|
perPage,
|
||||||
totalPages: Math.ceil(count / perPage),
|
totalPages: Math.ceil(count / perPage),
|
||||||
};
|
} satisfies FindResultSet<typeof maskedData>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,30 +1,88 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { SigningStatus, User } from '@documenso/prisma/client';
|
||||||
|
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||||
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
|
||||||
export type GetStatsInput = {
|
export type GetStatsInput = {
|
||||||
userId: number;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStats = async ({ userId }: GetStatsInput) => {
|
export const getStats = async ({ user }: GetStatsInput) => {
|
||||||
const result = await prisma.document.groupBy({
|
const [ownerCounts, notSignedCounts, hasSignedCounts] = await Promise.all([
|
||||||
|
prisma.document.groupBy({
|
||||||
by: ['status'],
|
by: ['status'],
|
||||||
_count: {
|
_count: {
|
||||||
_all: true,
|
_all: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
|
prisma.document.groupBy({
|
||||||
|
by: ['status'],
|
||||||
|
_count: {
|
||||||
|
_all: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
status: ExtendedDocumentStatus.PENDING,
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
signingStatus: SigningStatus.NOT_SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.document.groupBy({
|
||||||
|
by: ['status'],
|
||||||
|
_count: {
|
||||||
|
_all: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
status: {
|
||||||
|
not: ExtendedDocumentStatus.DRAFT,
|
||||||
|
},
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const stats: Record<DocumentStatus, number> = {
|
const stats: Record<ExtendedDocumentStatus, number> = {
|
||||||
[DocumentStatus.DRAFT]: 0,
|
[ExtendedDocumentStatus.DRAFT]: 0,
|
||||||
[DocumentStatus.PENDING]: 0,
|
[ExtendedDocumentStatus.PENDING]: 0,
|
||||||
[DocumentStatus.COMPLETED]: 0,
|
[ExtendedDocumentStatus.COMPLETED]: 0,
|
||||||
|
[ExtendedDocumentStatus.INBOX]: 0,
|
||||||
|
[ExtendedDocumentStatus.ALL]: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
result.forEach((stat) => {
|
ownerCounts.forEach((stat) => {
|
||||||
stats[stat.status] = stat._count._all;
|
stats[stat.status] = stat._count._all;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
notSignedCounts.forEach((stat) => {
|
||||||
|
stats[ExtendedDocumentStatus.INBOX] += stat._count._all;
|
||||||
|
});
|
||||||
|
|
||||||
|
hasSignedCounts.forEach((stat) => {
|
||||||
|
if (stat.status === ExtendedDocumentStatus.COMPLETED) {
|
||||||
|
stats[ExtendedDocumentStatus.COMPLETED] += stat._count._all;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.status === ExtendedDocumentStatus.PENDING) {
|
||||||
|
stats[ExtendedDocumentStatus.PENDING] += stat._count._all;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(stats).forEach((key) => {
|
||||||
|
if (key !== ExtendedDocumentStatus.ALL && isExtendedDocumentStatus(key)) {
|
||||||
|
stats[ExtendedDocumentStatus.ALL] += stats[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type FindResultSet<T> = {
|
export type FindResultSet<T> = {
|
||||||
data: T[];
|
data: T extends Array<any> ? T : T[];
|
||||||
count: number;
|
count: number;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
perPage: number;
|
perPage: number;
|
||||||
|
|||||||
11
packages/prisma/guards/is-extended-document-status.ts
Normal file
11
packages/prisma/guards/is-extended-document-status.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { ExtendedDocumentStatus } from '../types/extended-document-status';
|
||||||
|
|
||||||
|
export const isExtendedDocumentStatus = (value: unknown): value is ExtendedDocumentStatus => {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're using the assertion for a type-guard so it's safe to ignore the eslint warning
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
return Object.values(ExtendedDocumentStatus).includes(value as ExtendedDocumentStatus);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Document, Recipient } from '@documenso/prisma/client';
|
import { Document, Recipient } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type DocumentWithReciepient = Document & {
|
export type DocumentWithRecipient = Document & {
|
||||||
Recipient: Recipient[];
|
Recipient: Recipient[];
|
||||||
};
|
};
|
||||||
|
|||||||
12
packages/prisma/types/document.ts
Normal file
12
packages/prisma/types/document.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Document, Recipient } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export type DocumentWithRecipientAndSender = Omit<Document, 'document'> & {
|
||||||
|
recipient: Recipient;
|
||||||
|
sender: {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
subject: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
10
packages/prisma/types/extended-document-status.ts
Normal file
10
packages/prisma/types/extended-document-status.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { DocumentStatus } from '@prisma/client';
|
||||||
|
|
||||||
|
export const ExtendedDocumentStatus = {
|
||||||
|
...DocumentStatus,
|
||||||
|
INBOX: 'INBOX',
|
||||||
|
ALL: 'ALL',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ExtendedDocumentStatus =
|
||||||
|
(typeof ExtendedDocumentStatus)[keyof typeof ExtendedDocumentStatus];
|
||||||
@@ -115,6 +115,11 @@ module.exports = {
|
|||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
},
|
},
|
||||||
|
screens: {
|
||||||
|
'3xl': '1920px',
|
||||||
|
'4xl': '2560px',
|
||||||
|
'5xl': '3840px',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
|
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
|
||||||
|
|||||||
9
packages/tailwind-config/tsconfig.json
Normal file
9
packages/tailwind-config/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@documenso/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"noEmit": true,
|
||||||
|
},
|
||||||
|
"include": ["**/*.cjs", "**/*.js"],
|
||||||
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ const t = initTRPC.context<TrpcContext>().create({
|
|||||||
/**
|
/**
|
||||||
* Middlewares
|
* Middlewares
|
||||||
*/
|
*/
|
||||||
export const authenticatedMiddleware = t.middleware(({ ctx, next }) => {
|
export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
|
||||||
if (!ctx.session) {
|
if (!ctx.session) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
@@ -18,7 +18,7 @@ export const authenticatedMiddleware = t.middleware(({ ctx, next }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return next({
|
return await next({
|
||||||
ctx: {
|
ctx: {
|
||||||
...ctx,
|
...ctx,
|
||||||
|
|
||||||
|
|||||||
8
packages/tsconfig/tsconfig.json
Normal file
8
packages/tsconfig/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
|
||||||
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
}
|
||||||
28
packages/ui/icons/signature.tsx
Normal file
28
packages/ui/icons/signature.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||||
|
|
||||||
|
export const SignatureIcon: LucideIcon = ({
|
||||||
|
size = 24,
|
||||||
|
color = 'currentColor',
|
||||||
|
strokeWidth = 1.33,
|
||||||
|
absoluteStrokeWidth,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1.5 11H14.5M1.5 14C1.5 14 8.72 2 4.86938 2H4.875C2.01 2 1.97437 14.0694 8 6.51188V6.5C8 6.5 9 11.3631 11.5 7.52375V7.5C11.5 7.5 11.5 9 14.5 9"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -86,7 +86,7 @@ export const DocumentDropzone = ({ className, onDrop, ...props }: DocumentDropzo
|
|||||||
multiple: false,
|
multiple: false,
|
||||||
onDrop: ([acceptedFile]) => {
|
onDrop: ([acceptedFile]) => {
|
||||||
if (acceptedFile && onDrop) {
|
if (acceptedFile && onDrop) {
|
||||||
onDrop(acceptedFile);
|
void onDrop(acceptedFile);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ export const AddFieldsFormPartial = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = handleSubmit(onSubmit);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
append,
|
append,
|
||||||
remove,
|
remove,
|
||||||
@@ -500,7 +502,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onGoBackClick={documentFlow.onBackStep}
|
onGoBackClick={documentFlow.onBackStep}
|
||||||
onGoNextClick={() => handleSubmit(onSubmit)()}
|
onGoNextClick={() => void onFormSubmit()}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export const AddSignersFormPartial = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = handleSubmit(onSubmit);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
append: appendSigner,
|
append: appendSigner,
|
||||||
fields: signers,
|
fields: signers,
|
||||||
@@ -214,7 +216,7 @@ export const AddSignersFormPartial = ({
|
|||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onGoBackClick={documentFlow.onBackStep}
|
onGoBackClick={documentFlow.onBackStep}
|
||||||
onGoNextClick={() => handleSubmit(onSubmit)()}
|
onGoNextClick={() => void onFormSubmit()}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export const AddSubjectFormPartial = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = handleSubmit(onSubmit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
@@ -130,7 +132,7 @@ export const AddSubjectFormPartial = ({
|
|||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
|
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
|
||||||
onGoBackClick={documentFlow.onBackStep}
|
onGoBackClick={documentFlow.onBackStep}
|
||||||
onGoNextClick={() => handleSubmit(onSubmit)()}
|
onGoNextClick={() => void onFormSubmit()}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const PDFViewer = ({ className, document, onPageClick, ...props }: PDFVie
|
|||||||
const pageY = event.clientY - top;
|
const pageY = event.clientY - top;
|
||||||
|
|
||||||
if (onPageClick) {
|
if (onPageClick) {
|
||||||
onPageClick({
|
void onPageClick({
|
||||||
pageNumber,
|
pageNumber,
|
||||||
numPages,
|
numPages,
|
||||||
originalEvent: event,
|
originalEvent: event,
|
||||||
|
|||||||
@@ -302,8 +302,8 @@ export class Canvas {
|
|||||||
/**
|
/**
|
||||||
* Retrieves the signature as an image blob.
|
* Retrieves the signature as an image blob.
|
||||||
*/
|
*/
|
||||||
public toBlob(type?: string, quality?: number): Promise<Blob> {
|
public async toBlob(type?: string, quality?: number): Promise<Blob> {
|
||||||
return new Promise((resolve, reject) => {
|
const promise = new Promise<Blob>((resolve, reject) => {
|
||||||
this.$canvas.toBlob(
|
this.$canvas.toBlob(
|
||||||
(blob) => {
|
(blob) => {
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
@@ -317,5 +317,7 @@ export class Canvas {
|
|||||||
quality,
|
quality,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return await promise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user