Compare commits
58 Commits
chore/redu
...
feat/blog-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7722e63e1b | ||
|
|
14fd0eb906 | ||
|
|
af6c62d0bf | ||
|
|
8d7d6a19e7 | ||
|
|
463dc48ea6 | ||
|
|
d8f6a25059 | ||
|
|
93962625ed | ||
|
|
249211bd4f | ||
|
|
bfe0d50661 | ||
|
|
5d4a07bcc5 | ||
|
|
d28bb5de99 | ||
|
|
83a83164d4 | ||
|
|
d71e43c5d6 | ||
|
|
ed6fa4dc2a | ||
|
|
4f3970c361 | ||
|
|
40767430d9 | ||
|
|
1edfe9548d | ||
|
|
fead48c2f0 | ||
|
|
0abd3da7fd | ||
|
|
2f78922421 | ||
|
|
3df0f61947 | ||
|
|
8e42dcb7ee | ||
|
|
1888ee97e6 | ||
|
|
068aef665d | ||
|
|
2772fc1678 | ||
|
|
8c4120f0a2 | ||
|
|
9f93af6134 | ||
|
|
68a5a9da1e | ||
|
|
1f8d5e45e1 | ||
|
|
8fd9730e2b | ||
|
|
04f6df6839 | ||
|
|
ca40e983e3 | ||
|
|
ba054ae915 | ||
|
|
e8336ae9b4 | ||
|
|
aad52a3e2e | ||
|
|
829122c486 | ||
|
|
090752c539 | ||
|
|
fad6414995 | ||
|
|
c817c67a1c | ||
|
|
c7001e62f3 | ||
|
|
bf71d2a14e | ||
|
|
163911255e | ||
|
|
24e38a3bbc | ||
|
|
dfd714f16a | ||
|
|
722081f89e | ||
|
|
f0e1df22b8 | ||
|
|
615cb263fb | ||
|
|
18faaf49d9 | ||
|
|
650b69ae56 | ||
|
|
eb4be963e3 | ||
|
|
27c27743e3 | ||
|
|
92930a2f63 | ||
|
|
7ad3365b0e | ||
|
|
f8bf4fea36 | ||
|
|
10cd8144eb | ||
|
|
66973a3745 | ||
|
|
85677bb792 | ||
|
|
7ae99d2038 |
7
.eslintignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Config files
|
||||||
|
*.config.js
|
||||||
|
*.config.cjs
|
||||||
|
|
||||||
|
# Statically hosted javascript files
|
||||||
|
apps/*/public/*.js
|
||||||
|
apps/*/public/*.cjs
|
||||||
21
.github/workflows/semantic-pull-requests.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: "Validate PR Name"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-pr:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"recharts": "^2.7.2",
|
"recharts": "^2.7.2",
|
||||||
|
"sharp": "0.32.5",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 } }) => {
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { ImageResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { allBlogPosts } from 'contentlayer/generated';
|
||||||
|
|
||||||
|
export const runtime = 'edge';
|
||||||
|
|
||||||
|
export const size = {
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const contentType = 'image/png';
|
||||||
|
|
||||||
|
type BlogPostOpenGraphImageProps = {
|
||||||
|
params: { post: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function BlogPostOpenGraphImage({ params }: BlogPostOpenGraphImageProps) {
|
||||||
|
const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`);
|
||||||
|
|
||||||
|
if (!blogPost) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The long urls are needed for a compiler optimisation on the Next.js side, lifting this up
|
||||||
|
// to a constant will break og image generation.
|
||||||
|
const [interBold, interRegular, backgroundImage, logoImage] = await Promise.all([
|
||||||
|
fetch(new URL('./../../../../assets/inter-bold.ttf', import.meta.url)).then(async (res) =>
|
||||||
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
|
fetch(new URL('./../../../../assets/inter-regular.ttf', import.meta.url)).then(async (res) =>
|
||||||
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
|
fetch(new URL('./../../../../assets/background-blog-og.png', import.meta.url)).then(
|
||||||
|
async (res) => res.arrayBuffer(),
|
||||||
|
),
|
||||||
|
fetch(new URL('./../../../../../public/logo.png', import.meta.url)).then(async (res) =>
|
||||||
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div tw="relative h-full w-full flex flex-col items-center justify-center text-center">
|
||||||
|
{/* @ts-expect-error Lack of typing from ImageResponse */}
|
||||||
|
<img src={backgroundImage} alt="og-background" tw="absolute inset-0 w-full h-full" />
|
||||||
|
|
||||||
|
{/* @ts-expect-error Lack of typing from ImageResponse */}
|
||||||
|
<img src={logoImage} alt="logo" tw="h-8" />
|
||||||
|
|
||||||
|
<h1 tw="mt-8 text-6xl text-center flex items-center justify-center w-full max-w-[800px] font-bold text-center mx-auto">
|
||||||
|
{blogPost.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p tw="font-normal">Written by {blogPost.authorName}</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
...size,
|
||||||
|
fonts: [
|
||||||
|
{
|
||||||
|
name: 'Inter',
|
||||||
|
data: interRegular,
|
||||||
|
style: 'normal',
|
||||||
|
weight: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Inter',
|
||||||
|
data: interBold,
|
||||||
|
style: 'normal',
|
||||||
|
weight: 700,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 } }) => {
|
||||||
@@ -17,7 +17,9 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { title: `Documenso - ${blogPost.title}` };
|
return {
|
||||||
|
title: `Documenso - ${blogPost.title}`,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mdxComponents: MDXComponents = {
|
const mdxComponents: MDXComponents = {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
BIN
apps/marketing/src/assets/background-blog-og.png
Normal file
|
After Width: | Height: | Size: 896 KiB |
BIN
apps/marketing/src/assets/inter-bold.ttf
Normal file
BIN
apps/marketing/src/assets/inter-regular.ttf
Normal file
@@ -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;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@documenso/ee": "*",
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
@@ -21,6 +22,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",
|
||||||
@@ -36,12 +38,14 @@
|
|||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
|
"sharp": "0.32.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"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} />
|
||||||
|
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
{value !== ExtendedDocumentStatus.ALL && (
|
||||||
{Math.min(stats.PENDING, 99)}
|
<span className="ml-1 hidden opacity-50 md:inline-block">
|
||||||
</span>
|
{Math.min(stats[value], 99)}
|
||||||
</Link>
|
</span>
|
||||||
</TabsTrigger>
|
)}
|
||||||
|
</Link>
|
||||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.COMPLETED} asChild>
|
</TabsTrigger>
|
||||||
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
||||||
|
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
|
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
|
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import { PasswordForm } from '~/components/forms/password';
|
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||||
import { getServerComponentFlag } from '~/helpers/get-server-component-feature-flag';
|
import { getServerComponentFlag } from '~/helpers/get-server-component-feature-flag';
|
||||||
|
|
||||||
export default async function BillingSettingsPage() {
|
export default async function BillingSettingsPage() {
|
||||||
@@ -15,17 +21,55 @@ export default async function BillingSettingsPage() {
|
|||||||
redirect('/settings/profile');
|
redirect('/settings/profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let subscription = await getSubscriptionByUserId({ userId: user.id });
|
||||||
|
|
||||||
|
// If we don't have a customer record, create one as well as an empty subscription.
|
||||||
|
if (!subscription?.customerId) {
|
||||||
|
subscription = await createCustomer({ user });
|
||||||
|
}
|
||||||
|
|
||||||
|
let billingPortalUrl = '';
|
||||||
|
|
||||||
|
if (subscription?.customerId) {
|
||||||
|
billingPortalUrl = await getPortalSession({
|
||||||
|
customerId: subscription.customerId,
|
||||||
|
returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Billing</h3>
|
<h3 className="text-lg font-medium">Billing</h3>
|
||||||
|
|
||||||
<p className="mt-2 text-sm text-slate-500">
|
<p className="mt-2 text-sm text-slate-500">
|
||||||
Here you can update and manage your subscription.
|
Your subscription is{' '}
|
||||||
|
{subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}.
|
||||||
|
{subscription?.periodEnd && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
Your next payment is due on{' '}
|
||||||
|
<span className="font-semibold">
|
||||||
|
<LocaleDate date={subscription.periodEnd} />
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="my-4" />
|
<hr className="my-4" />
|
||||||
|
|
||||||
<PasswordForm user={user} className="max-w-xl" />
|
{billingPortalUrl && (
|
||||||
|
<Button asChild>
|
||||||
|
<Link href={billingPortalUrl}>Manage Subscription</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!billingPortalUrl && (
|
||||||
|
<p className="max-w-[60ch] text-base text-slate-500">
|
||||||
|
You do not currently have a customer record, this should not happen. Please contact
|
||||||
|
support for assistance.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
|
|
||||||
<hr className="border-border mb-8 mt-4" />
|
<hr className="border-border mb-8 mt-4" />
|
||||||
|
|
||||||
<div className="-mx-2 flex flex-1 flex-col overflow-y-auto px-2">
|
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="full-name">Full Name</Label>
|
<Label htmlFor="full-name">Full Name</Label>
|
||||||
@@ -98,10 +98,10 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex flex-col gap-4 md:flex-row">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
@@ -109,8 +109,8 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
className="w-full"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex-1"
|
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={!isComplete || isSubmitting}
|
disabled={!isComplete || isSubmitting}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 grid grid-cols-12 gap-8">
|
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">
|
||||||
<Card
|
<Card
|
||||||
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
|
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
|
||||||
gradient
|
gradient
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 394 KiB After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 20 MiB After Width: | Height: | Size: 14 MiB |
@@ -7,13 +7,15 @@ 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(
|
||||||
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 ',
|
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
||||||
{
|
{
|
||||||
'text-foreground': pathname?.startsWith('/documents'),
|
'text-foreground': pathname?.startsWith('/documents'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 && (
|
||||||
className={cn('mr-2 inline-block h-4 w-4', {
|
<Icon
|
||||||
[color]: !inheritColor,
|
className={cn('mr-2 inline-block h-4 w-4', {
|
||||||
})}
|
[color]: !inheritColor,
|
||||||
/>
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import { FormErrorMessage } from '../form/form-error-message';
|
|||||||
|
|
||||||
export const ZPasswordFormSchema = z
|
export const ZPasswordFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
password: z.string().min(6),
|
password: z.string().min(6).max(72),
|
||||||
repeatedPassword: z.string().min(6),
|
repeatedPassword: z.string().min(6).max(72),
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.repeatedPassword, {
|
.refine((data) => data.password === data.repeatedPassword, {
|
||||||
message: 'Passwords do not match',
|
message: 'Passwords do not match',
|
||||||
@@ -39,6 +39,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
reset,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<TPasswordFormSchema>({
|
} = useForm<TPasswordFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
@@ -56,6 +57,8 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
reset();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Password updated',
|
title: 'Password updated',
|
||||||
description: 'Your password has been updated successfully.',
|
description: 'Your password has been updated successfully.',
|
||||||
@@ -73,7 +76,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
title: 'An unknown error occurred',
|
title: 'An unknown error occurred',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
description:
|
description:
|
||||||
'We encountered an unknown error while attempting to sign you In. Please try again later.',
|
'We encountered an unknown error while attempting to update your password. Please try again later.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,6 +95,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
minLength={6}
|
||||||
|
maxLength={72}
|
||||||
|
autoComplete="new-password"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
@@ -107,6 +113,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
<Input
|
<Input
|
||||||
id="repeated-password"
|
id="repeated-password"
|
||||||
type="password"
|
type="password"
|
||||||
|
minLength={6}
|
||||||
|
maxLength={72}
|
||||||
|
autoComplete="new-password"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
{...register('repeatedPassword')}
|
{...register('repeatedPassword')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
export const ZSignInFormSchema = z.object({
|
export const ZSignInFormSchema = z.object({
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
password: z.string().min(1),
|
password: z.string().min(6).max(72),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSignInFormSchema = z.infer<typeof ZSignInFormSchema>;
|
export type TSignInFormSchema = z.infer<typeof ZSignInFormSchema>;
|
||||||
@@ -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">
|
||||||
@@ -99,6 +96,9 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
minLength={6}
|
||||||
|
maxLength={72}
|
||||||
|
autoComplete="current-password"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
export const ZSignUpFormSchema = z.object({
|
export const ZSignUpFormSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
password: z.string().min(1),
|
password: z.string().min(6).max(72),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
||||||
@@ -105,6 +105,9 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
minLength={6}
|
||||||
|
maxLength={72}
|
||||||
|
autoComplete="new-password"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
|
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
|
||||||
|
|
||||||
import { TFeatureFlagValue, ZFeatureFlagValueSchema } from '~/providers/feature-flag';
|
import { TFeatureFlagValue, ZFeatureFlagValueSchema } from '~/providers/feature-flag.types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate whether a flag is enabled for the current user.
|
* Evaluate whether a flag is enabled for the current user.
|
||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
import formidable from 'formidable';
|
import formidable, { type File } from 'formidable';
|
||||||
import { type File } from 'formidable';
|
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
import { getServerSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getServerSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FEATURE_FLAG_POLL_INTERVAL,
|
FEATURE_FLAG_POLL_INTERVAL,
|
||||||
LOCAL_FEATURE_FLAGS,
|
LOCAL_FEATURE_FLAGS,
|
||||||
@@ -12,14 +10,7 @@ import {
|
|||||||
|
|
||||||
import { getAllFlags } from '~/helpers/get-feature-flag';
|
import { getAllFlags } from '~/helpers/get-feature-flag';
|
||||||
|
|
||||||
export const ZFeatureFlagValueSchema = z.union([
|
import { TFeatureFlagValue } from './feature-flag.types';
|
||||||
z.boolean(),
|
|
||||||
z.string(),
|
|
||||||
z.number(),
|
|
||||||
z.undefined(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export type TFeatureFlagValue = z.infer<typeof ZFeatureFlagValueSchema>;
|
|
||||||
|
|
||||||
export type FeatureFlagContextValue = {
|
export type FeatureFlagContextValue = {
|
||||||
getFlag: (_key: string) => TFeatureFlagValue;
|
getFlag: (_key: string) => TFeatureFlagValue;
|
||||||
@@ -67,7 +58,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 +75,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);
|
||||||
|
|
||||||
|
|||||||
10
apps/web/src/providers/feature-flag.types.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const ZFeatureFlagValueSchema = z.union([
|
||||||
|
z.boolean(),
|
||||||
|
z.string(),
|
||||||
|
z.number(),
|
||||||
|
z.undefined(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type TFeatureFlagValue = z.infer<typeof ZFeatureFlagValueSchema>;
|
||||||
@@ -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}`,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
1047
package-lock.json
generated
@@ -18,14 +18,14 @@
|
|||||||
"@commitlint/config-conventional": "^17.7.0",
|
"@commitlint/config-conventional": "^17.7.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"dotenv-cli": "^7.2.1",
|
"dotenv-cli": "^7.2.1",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^8.40.0",
|
||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"lint-staged": "^14.0.0",
|
"lint-staged": "^14.0.0",
|
||||||
"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/*",
|
||||||
|
|||||||
40
packages/ee/LICENSE
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
The Documenso Commercial License (the “Commercial License”)
|
||||||
|
Copyright (c) 2023 Documenso, Inc
|
||||||
|
|
||||||
|
With regard to the Documenso Software:
|
||||||
|
|
||||||
|
This software and associated documentation files (the "Software") may only be
|
||||||
|
used in production, if you (and any entity that you represent) have agreed to,
|
||||||
|
and are in compliance with, an agreement governing
|
||||||
|
the use of the Software, as mutually agreed by you and Documenso, Inc ("Documenso"),
|
||||||
|
and otherwise have a valid Documenso Enterprise Edition subscription ("Commercial Subscription").
|
||||||
|
Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software.
|
||||||
|
You agree that Documenso and/or its licensors (as applicable) retain all right, title and interest in
|
||||||
|
and to all such modifications and/or patches, and all such modifications and/or
|
||||||
|
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||||
|
exploited with a valid Commercial Subscription for the correct number of hosts.
|
||||||
|
Notwithstanding the foregoing, you may copy and modify the Software for development
|
||||||
|
and testing purposes, without requiring a subscription. You agree that Documenso and/or
|
||||||
|
its licensors (as applicable) retain all right, title and interest in and to all such
|
||||||
|
modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||||
|
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell the Software.
|
||||||
|
|
||||||
|
This Commercial License applies only to the part of this Software that is not distributed under
|
||||||
|
the AGPLv3 license. Any part of this Software distributed under the MIT license or which
|
||||||
|
is served client-side as an image, font, cascading stylesheet (CSS), file which produces
|
||||||
|
or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or
|
||||||
|
in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall
|
||||||
|
be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
For all third party components incorporated into the Documenso Software, those
|
||||||
|
components are licensed under the original license provided by the owner of the
|
||||||
|
applicable component.
|
||||||
1
packages/ee/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
17
packages/ee/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@documenso/ee",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "./index.ts",
|
||||||
|
"types": "./index.ts",
|
||||||
|
"license": "COMMERCIAL",
|
||||||
|
"files": [
|
||||||
|
"client-only/",
|
||||||
|
"server-only/",
|
||||||
|
"universal/"
|
||||||
|
],
|
||||||
|
"scripts": {},
|
||||||
|
"dependencies": {
|
||||||
|
"@documenso/lib": "*",
|
||||||
|
"@documenso/prisma": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
packages/ee/server-only/stripe/create-customer.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export type CreateCustomerOptions = {
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCustomer = async ({ user }: CreateCustomerOptions) => {
|
||||||
|
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
|
||||||
|
|
||||||
|
if (existingSubscription) {
|
||||||
|
throw new Error('User already has a subscription');
|
||||||
|
}
|
||||||
|
|
||||||
|
const customer = await stripe.customers.create({
|
||||||
|
name: user.name ?? undefined,
|
||||||
|
email: user.email,
|
||||||
|
metadata: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await prisma.subscription.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
customerId: customer.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
19
packages/ee/server-only/stripe/get-portal-session.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
|
||||||
|
export type GetPortalSessionOptions = {
|
||||||
|
customerId: string;
|
||||||
|
returnUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPortalSession = async ({ customerId, returnUrl }: GetPortalSessionOptions) => {
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
const session = await stripe.billingPortal.sessions.create({
|
||||||
|
customer: customerId,
|
||||||
|
return_url: returnUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
return session.url;
|
||||||
|
};
|
||||||
5
packages/ee/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "@documenso/tsconfig/react-library.json",
|
||||||
|
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"],
|
||||||
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
}
|
||||||
@@ -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
@@ -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" />
|
<Img
|
||||||
|
src={getAssetUrl('/static/logo.png')}
|
||||||
|
alt="Documenso Logo"
|
||||||
|
className="mb-4 h-6"
|
||||||
|
/>
|
||||||
|
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<TemplateDocumentCompleted
|
||||||
<div className="flex items-center justify-center p-4">
|
downloadLink={downloadLink}
|
||||||
<Img
|
reviewLink={reviewLink}
|
||||||
className="h-42"
|
documentName={documentName}
|
||||||
src={getAssetUrl('/static/document.png')}
|
assetBaseUrl={assetBaseUrl}
|
||||||
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>
|
|
||||||
</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" />
|
<Img
|
||||||
|
src={getAssetUrl('/static/logo.png')}
|
||||||
|
alt="Documenso Logo"
|
||||||
|
className="mb-4 h-6"
|
||||||
|
/>
|
||||||
|
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<TemplateDocumentInvite
|
||||||
<div className="flex items-center justify-center p-4">
|
inviterName={inviterName}
|
||||||
<Img
|
inviterEmail={inviterEmail}
|
||||||
className="h-42"
|
documentName={documentName}
|
||||||
src={getAssetUrl('/static/document.png')}
|
signDocumentLink={signDocumentLink}
|
||||||
alt="Documenso"
|
assetBaseUrl={assetBaseUrl}
|
||||||
/>
|
/>
|
||||||
</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>
|
|
||||||
</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" />
|
<Img
|
||||||
|
src={getAssetUrl('/static/logo.png')}
|
||||||
|
alt="Documenso Logo"
|
||||||
|
className="mb-4 h-6"
|
||||||
|
/>
|
||||||
|
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<TemplateDocumentPending documentName={documentName} assetBaseUrl={assetBaseUrl} />
|
||||||
<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>
|
|
||||||
</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);
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ module.exports = {
|
|||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:prettier/recommended',
|
'plugin:prettier/recommended',
|
||||||
|
'plugin:package-json/recommended',
|
||||||
],
|
],
|
||||||
|
|
||||||
plugins: ['prettier'],
|
plugins: ['prettier', 'package-json'],
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
@@ -19,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,
|
||||||
@@ -32,6 +35,32 @@ 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: '^_' }],
|
||||||
|
|
||||||
|
'no-duplicate-imports': 'error',
|
||||||
|
'no-multi-spaces': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ignoreEOLComments: false,
|
||||||
|
exceptions: {
|
||||||
|
BinaryExpression: false,
|
||||||
|
VariableDeclarator: false,
|
||||||
|
ImportDeclaration: false,
|
||||||
|
Property: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// 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,9 +9,10 @@
|
|||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
"eslint-config-next": "13.4.12",
|
"eslint-config-next": "13.4.12",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-config-turbo": "^1.9.3",
|
||||||
|
"eslint-plugin-package-json": "^0.1.4",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"eslint-config-turbo": "^1.9.3",
|
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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"]
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import GoogleProvider, { GoogleProfile } from 'next-auth/providers/google';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
||||||
|
import { ErrorCodes } from './error-codes';
|
||||||
|
|
||||||
export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
@@ -23,21 +24,23 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
|||||||
},
|
},
|
||||||
authorize: async (credentials, _req) => {
|
authorize: async (credentials, _req) => {
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
return null;
|
throw new Error(ErrorCodes.CredentialsNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { email, password } = credentials;
|
const { email, password } = credentials;
|
||||||
|
|
||||||
const user = await getUserByEmail({ email }).catch(() => null);
|
const user = await getUserByEmail({ email }).catch(() => {
|
||||||
|
throw new Error(ErrorCodes.IncorrectEmailPassword);
|
||||||
|
});
|
||||||
|
|
||||||
if (!user || !user.password) {
|
if (!user.password) {
|
||||||
return null;
|
throw new Error(ErrorCodes.UserMissingPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPasswordsSame = compare(password, user.password);
|
const isPasswordsSame = await compare(password, user.password);
|
||||||
|
|
||||||
if (!isPasswordsSame) {
|
if (!isPasswordsSame) {
|
||||||
return null;
|
throw new Error(ErrorCodes.IncorrectEmailPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -86,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,
|
||||||
|
|||||||
5
packages/lib/next-auth/error-codes.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const ErrorCodes = {
|
||||||
|
IncorrectEmailPassword: 'incorrect-email-password',
|
||||||
|
UserMissingPassword: 'missing-password',
|
||||||
|
CredentialsNotFound: 'credentials-not-found',
|
||||||
|
} as const;
|
||||||
@@ -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: {
|
||||||
|
contains: term,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
} as const);
|
||||||
|
|
||||||
if (term) {
|
const filters = match<ExtendedDocumentStatus, Prisma.DocumentWhereInput>(status)
|
||||||
filters.title = {
|
.with(ExtendedDocumentStatus.ALL, () => ({
|
||||||
contains: term,
|
OR: [
|
||||||
mode: 'insensitive',
|
{
|
||||||
};
|
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([
|
||||||
by: ['status'],
|
prisma.document.groupBy({
|
||||||
_count: {
|
by: ['status'],
|
||||||
_all: true,
|
_count: {
|
||||||
},
|
_all: true,
|
||||||
where: {
|
},
|
||||||
userId,
|
where: {
|
||||||
},
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export type GetSubscriptionByUserIdOptions = {
|
||||||
|
userId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSubscriptionByUserId = ({ userId }: GetSubscriptionByUserIdOptions) => {
|
||||||
|
return prisma.subscription.findFirst({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { hash } from 'bcrypt';
|
import { compare, hash } from 'bcrypt';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export type UpdatePasswordOptions = {
|
|||||||
|
|
||||||
export const updatePassword = async ({ userId, password }: UpdatePasswordOptions) => {
|
export const updatePassword = async ({ userId, password }: UpdatePasswordOptions) => {
|
||||||
// Existence check
|
// Existence check
|
||||||
await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
@@ -19,6 +19,13 @@ export const updatePassword = async ({ userId, password }: UpdatePasswordOptions
|
|||||||
|
|
||||||
const hashedPassword = await hash(password, SALT_ROUNDS);
|
const hashedPassword = await hash(password, SALT_ROUNDS);
|
||||||
|
|
||||||
|
// Compare the new password with the old password
|
||||||
|
const isSamePassword = await compare(password, user.password as string);
|
||||||
|
|
||||||
|
if (isSamePassword) {
|
||||||
|
throw new Error('Your new password cannot be the same as your old password.');
|
||||||
|
}
|
||||||
|
|
||||||
const updatedUser = await prisma.user.update({
|
const updatedUser = await prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
};
|
||||||
5
packages/prisma/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "@documenso/tsconfig/react-library.json",
|
||||||
|
"include": ["."],
|
||||||
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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')],
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint": "7.32.0",
|
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
"tailwindcss-animate": "^1.0.5"
|
"tailwindcss-animate": "^1.0.5"
|
||||||
|
|||||||
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"]
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'zod';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const ZSignUpMutationSchema = z.object({
|
export const ZSignUpMutationSchema = z.object({
|
||||||
|
|||||||
@@ -40,12 +40,16 @@ export const profileRouter = router({
|
|||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
let message =
|
||||||
|
'We were unable to update your profile. Please review the information you provided and try again.';
|
||||||
|
|
||||||
|
if (err instanceof Error) {
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
message:
|
message,
|
||||||
'We were unable to update your profile. Please review the information you provided and try again.',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||