Compare commits

...

39 Commits

Author SHA1 Message Date
Timur Ercan dbb01fb90f fix: links 2023-09-22 19:46:36 +02:00
Timur Ercan 284442cad7 feat: open sourcing documensos design article 2023-09-22 18:26:45 +02:00
Lucas Smith 1e5ecd79c2 Merge pull request #354 from documenso/feat/table-empty-state
feat: add empty document state
2023-09-22 23:36:42 +10:00
Lucas Smith 58f10268e2 Merge pull request #359 from documenso/feat/send-email
feat: send email when recipient is done signing and every recipient is done signing
2023-09-22 23:29:35 +10:00
Lucas Smith 43ce76d928 fix: remove unused import 2023-09-22 23:29:14 +10:00
Lucas Smith 201ba65e1c fix: remove unused import 2023-09-22 23:23:55 +10:00
Lucas Smith 0d130b17c8 fix: minor updates 2023-09-22 23:22:48 +10:00
Lucas Smith 2c90f767fd Merge branch 'feat/refresh' into feat/send-email 2023-09-22 23:19:43 +10:00
Lucas Smith 946b2fe129 fix: use named export 2023-09-22 23:15:30 +10:00
Lucas Smith 2a2dbb65c6 Merge branch 'feat/refresh' into feat/table-empty-state 2023-09-22 23:13:51 +10:00
Lucas Smith ef2300a600 Merge pull request #343 from adithyaakrishna/feat/404
feat: added custom `not-found` or 404 pages
2023-09-22 23:11:34 +10:00
Lucas Smith eea99ac871 Merge pull request #391 from documenso/feat/custom-emails
feat: send custom emails
2023-09-22 22:53:09 +10:00
Lucas Smith 78325d9b39 Merge branch 'feat/refresh' into feat/404 2023-09-22 15:01:49 +10:00
Lucas Smith b3f26055d9 Merge pull request #395 from nsylke/nsylke-patch-10
fix: remove disallow property of _next from robots
2023-09-21 22:36:30 +10:00
nsylke 181af24b78 fix: remove disallow property of _next from robots 2023-09-20 20:46:23 -05:00
Lucas Smith c247295131 Merge pull request #388 from captain-Akshay/feat/theme
feat: added a better theme change ability for user
2023-09-20 21:45:45 +10:00
captain-Akshay 7771d7acbe feat: added the icon for theme 2023-09-20 06:56:40 +05:30
captain-Akshay 4cd56fa98c feat: removed unecessary code to the file and made few UI changes 2023-09-19 19:45:21 +05:30
captain-Akshay a1ce6f696a feat: added a better theme change ability for user 2023-09-19 13:38:37 +05:30
Ephraim Atta-Duncan 776324c875 fix: fitler only unsigned documents 2023-09-17 14:38:39 +00:00
Ephraim Atta-Duncan 6f4c280583 chore: match file name and method name 2023-09-17 14:31:44 +00:00
Lucas Smith 2bad1b9f55 fix: tidy messaging 2023-09-09 03:45:15 +00:00
Lucas Smith 73b0dc315e fix: use ts-pattern 2023-09-09 03:31:17 +00:00
Ephraim Atta-Duncan 525ff21563 feat: avoid sending pending email to document with 1 recipients 2023-09-07 20:52:18 +00:00
Ephraim Atta-Duncan 863e53a2d5 refactor: pass document id as arguments 2023-09-07 20:38:18 +00:00
Ephraim Atta-Duncan da2033692c feat: send email when all recipients have signed 2023-09-07 20:14:04 +00:00
Ephraim Atta-Duncan dbbe17a0a8 feat: send email when recipient is done signing 2023-09-07 19:58:48 +00:00
Ephraim Atta-Duncan 0c145fab0b chore: fix eslint errors 2023-09-06 12:01:30 +00:00
Ephraim Atta-Duncan f6e49e3f21 chore: remove code from different branch 2023-09-06 12:00:23 +00:00
Ephraim Atta-Duncan 1f027d75d3 chore: fix eslint errors 2023-09-06 11:55:02 +00:00
Ephraim Atta-Duncan 1ba7767f8e chore: correct types on component 2023-09-06 11:52:15 +00:00
Ephraim Atta-Duncan 8220b2f086 feat: add empty state for different status 2023-09-06 11:47:58 +00:00
Ephraim Atta-Duncan a74374e39f feat: add initial empty state for no results 2023-09-06 11:11:13 +00:00
Ephraim Atta-Duncan 551918ab9b feat: redirect signed document to completed page 2023-09-05 13:53:18 +00:00
Mythie d524ea77ab fix: update styling 2023-09-05 13:15:45 +10:00
Adithya Krishna 2524458b0c chore: updated wording
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-09-03 23:59:02 +05:30
Adithya Krishna 12c45fb882 feat: added 404 page for web app
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-09-03 23:53:07 +05:30
Adithya Krishna 118483b6cc chore: updated 404 page for marketing app
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-09-03 23:52:51 +05:30
Adithya Krishna fd6350b397 feat: added 404 page for marketing app
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-09-03 23:30:48 +05:30
15 changed files with 441 additions and 43 deletions
@@ -0,0 +1,49 @@
---
title: Open Sourcing Documenso's Design
description: It's day 1 of our first launch week. We are kicking it off by open sourcing Documenso's design system! Let's go..
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2023-09-25
tags:
- Design
- Open Source
- Community
---
<figure>
<MdxNextImage
src="/blog/designsystem.png"
width="1260"
height="630"
alt="Documenso's Design System"
/>
<figcaption className="text-center">
Documenso's Design System ✨
</figcaption>
</figure>
> TLDR; Our design system is OSS under MIT at [design.documenso.com](https://design.documenso.com)
Today, we are open sourcing our design system, lovingly crafted by [Thilo](https://twitter.com/thilokonzok). The system is public on Figma and will be available at [design.documenso.com](https://design.documenso.com) from here on out.
We are publishing it under the MIT License so everyone can share, remix, and use it as it helps them most.
We chose to start our first launch week with a design topic to emphasize the role design will play in Documenso's company and community culture. As it is historically difficult to bring together open-source software with great design, this is our first step towards encouraging a more design-driven COSS (Commercial Open Source) movement.
## Designers Welcome
We added a designer role in our Discord to create a space for designers to explore and discuss design-related topics of Documenso and signing in general. In the future, we want to foster more coding contributions and start a design culture around the product. As it is much more difficult to incorporate design contributions, we have yet to find a clear plan of what that will look like. I would like to see contributions around stuff we are NOT working on. Designs in that area can inspire and start discussions without the complexities of implementing them immediately — a free-thinking space around everything Documenso. Having a free mandate to design without restriction can create many exciting ideas. Some Ideas worth exploring:
## Areas for design contributions
- Explorations of exciting aspects of signing and document handling:
- What does signing look like when we no longer have skeuomorphic signatures?
- What is signing if we move beyond paper-inspired documents?
- What would the government process look like using Documenso?
- Solutions Concepts for features further down the roadmap, e.g., Widgets in websites
These are fascinating ideas for explorative design. They won't be built 1:1 but shape how we think about signing and where it can go, which is even more critical. If you are interested in product design, you are invited to join our [Discord](https://documen.so/discord) and discuss the future of signing design or the future of Documenso's design system. Also let me know what you think on [X (formerly Twitter)](https://x.com/eltimuro).
Best from Hamburg\
Timur
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+65
View File
@@ -0,0 +1,65 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { ChevronLeft } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import backgroundPattern from '~/assets/background-pattern.png';
export default function NotFound() {
const router = useRouter();
return (
<div className={cn('relative max-w-[100vw] overflow-hidden')}>
<div className="absolute -inset-24 -z-10">
<motion.div
className="flex h-full w-full items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 0.8, transition: { duration: 0.5, delay: 0.5 } }}
>
<Image
src={backgroundPattern}
alt="background pattern"
className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover md:scale-100 lg:scale-[100%]"
priority
/>
</motion.div>
</div>
<div className="container mx-auto flex h-full min-h-screen items-center px-6 py-32">
<div>
<p className="text-muted-foreground font-semibold">404 Page not found</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
<p className="text-muted-foreground mt-4 text-sm">
The page you are looking for was moved, removed, renamed or might never have existed.
</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button
variant="ghost"
className="w-32"
onClick={() => {
void router.back();
}}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Go Back
</Button>
<Button className="w-32" asChild>
<Link href="/">Home</Link>
</Button>
</div>
</div>
</div>
</div>
);
}
+5 -5
View File
@@ -4,11 +4,11 @@ import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/*',
disallow: ['/_next/*'],
},
rules: [
{
userAgent: '*',
},
],
sitemap: `${getBaseUrl()}/sitemap.xml`,
};
}
@@ -0,0 +1,50 @@
import { Bird, CheckCircle2 } from 'lucide-react';
import { match } from 'ts-pattern';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
export type EmptyDocumentProps = { status: ExtendedDocumentStatus };
export const EmptyDocumentState = ({ status }: EmptyDocumentProps) => {
const {
title,
message,
icon: Icon,
} = match(status)
.with(ExtendedDocumentStatus.COMPLETED, () => ({
title: 'Nothing to do',
message:
'There are no completed documents yet. Documents that you have created or received that become completed will appear here later.',
icon: CheckCircle2,
}))
.with(ExtendedDocumentStatus.DRAFT, () => ({
title: 'No active drafts',
message:
'There are no active drafts at then current moment. You can upload a document to start drafting.',
icon: CheckCircle2,
}))
.with(ExtendedDocumentStatus.ALL, () => ({
title: "We're all empty",
message:
'You have not yet created or received any documents. To create a document please upload one.',
icon: Bird,
}))
.otherwise(() => ({
title: 'Nothing to do',
message:
'All documents are currently actioned. Any new documents are sent or recieved they will start to appear here.',
icon: CheckCircle2,
}));
return (
<div className="text-muted-foreground/60 flex h-60 flex-col items-center justify-center gap-y-4">
<Icon className="h-12 w-12" strokeWidth={1.5} />
<div className="text-center">
<h3 className="text-lg font-semibold">{title}</h3>
<p className="mt-2 max-w-[60ch]">{message}</p>
</div>
</div>
);
};
@@ -12,6 +12,7 @@ import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/ty
import { DocumentStatus } from '~/components/formatter/document-status';
import { DocumentsDataTable } from './data-table';
import { EmptyDocumentState } from './empty-state';
import { UploadDocument } from './upload-document';
export type DocumentsPageProps = {
@@ -96,7 +97,8 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
</div>
<div className="mt-8">
<DocumentsDataTable results={results} />
{results.count > 0 && <DocumentsDataTable results={results} />}
{results.count === 0 && <EmptyDocumentState status={status} />}
</div>
</div>
);
+26
View File
@@ -0,0 +1,26 @@
import Link from 'next/link';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { Button } from '@documenso/ui/primitives/button';
import NotFoundPartial from '~/components/partials/not-found';
export default async function NotFound() {
const session = await getServerComponentSession();
return (
<NotFoundPartial>
{session && (
<Button className="w-32" asChild>
<Link href="/documents">Documents</Link>
</Button>
)}
{!session && (
<Button className="w-32" asChild>
<Link href="/signin">Sign In</Link>
</Button>
)}
</NotFoundPartial>
);
}
@@ -10,6 +10,7 @@ import {
User as LucideUser,
Monitor,
Moon,
Palette,
Sun,
UserCog,
} from 'lucide-react';
@@ -26,7 +27,13 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
@@ -37,8 +44,8 @@ export type ProfileDropdownProps = {
};
export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
const { theme, setTheme } = useTheme();
const { getFlag } = useFeatureFlags();
const { theme, setTheme } = useTheme();
const isUserAdmin = isAdmin(user);
const isBillingEnabled = getFlag('app_billing');
@@ -98,28 +105,30 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
<DropdownMenuSeparator />
{theme === 'light' ? null : (
<DropdownMenuItem onClick={() => setTheme('light')}>
<Sun className="mr-2 h-4 w-4" />
Light Mode
</DropdownMenuItem>
)}
{theme === 'dark' ? null : (
<DropdownMenuItem onClick={() => setTheme('dark')}>
<Moon className="mr-2 h-4 w-4" />
Dark Mode
</DropdownMenuItem>
)}
{theme === 'system' ? null : (
<DropdownMenuItem onClick={() => setTheme('system')}>
<Monitor className="mr-2 h-4 w-4" />
System Theme
</DropdownMenuItem>
)}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Palette className="mr-2 h-4 w-4" />
Themes
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
<DropdownMenuRadioItem value="light">
<Sun className="mr-2 h-4 w-4" /> Light
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">
<Moon className="mr-2 h-4 w-4" />
Dark
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">
<Monitor className="mr-2 h-4 w-4" />
System
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="https://github.com/documenso/documenso" className="cursor-pointer">
<Github className="mr-2 h-4 w-4" />
-7
View File
@@ -1,7 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export * from 'framer-motion';
export const MotionDiv = motion.div;
@@ -0,0 +1,66 @@
'use client';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { ChevronLeft } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import backgroundPattern from '~/assets/background-pattern.png';
export type NotFoundPartialProps = {
children?: React.ReactNode;
};
export default function NotFoundPartial({ children }: NotFoundPartialProps) {
const router = useRouter();
return (
<div className={cn('relative max-w-[100vw] overflow-hidden')}>
<div className="absolute -inset-24 -z-10">
<motion.div
className="flex h-full w-full items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 0.8, transition: { duration: 0.5, delay: 0.5 } }}
>
<Image
src={backgroundPattern}
alt="background pattern"
className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover md:scale-100 lg:scale-[100%]"
priority
/>
</motion.div>
</div>
<div className="container mx-auto flex h-full min-h-screen items-center px-6 py-32">
<div>
<p className="text-muted-foreground font-semibold">404 Page not found</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
<p className="text-muted-foreground mt-4 text-sm">
The page you are looking for was moved, removed, renamed or might never have existed.
</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button
variant="ghost"
className="w-32"
onClick={() => {
void router.back();
}}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Go Back
</Button>
{children}
</div>
</div>
</div>
</div>
);
}
@@ -4,6 +4,7 @@ import { prisma } from '@documenso/prisma';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { sealDocument } from './seal-document';
import { sendPendingEmail } from './send-pending-email';
export type CompleteDocumentWithTokenOptions = {
token: string;
@@ -69,6 +70,19 @@ export const completeDocumentWithToken = async ({
},
});
const pendingRecipients = await prisma.recipient.count({
where: {
documentId: document.id,
signingStatus: {
not: SigningStatus.SIGNED,
},
},
});
if (pendingRecipients > 0) {
await sendPendingEmail({ documentId, recipientId: recipient.id });
}
const documents = await prisma.document.updateMany({
where: {
id: document.id,
@@ -9,6 +9,7 @@ import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { getFile } from '../../universal/upload/get-file';
import { putFile } from '../../universal/upload/put-file';
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
import { sendCompletedEmail } from './send-completed-email';
export type SealDocumentOptions = {
documentId: number;
@@ -86,4 +87,6 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => {
data: newData,
},
});
await sendCompletedEmail({ documentId });
};
@@ -0,0 +1,57 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
import { prisma } from '@documenso/prisma';
export interface SendDocumentOptions {
documentId: number;
}
export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) => {
const document = await prisma.document.findUnique({
where: {
id: documentId,
},
include: {
Recipient: true,
},
});
if (!document) {
throw new Error('Document not found');
}
if (document.Recipient.length === 0) {
throw new Error('Document has no recipients');
}
await Promise.all([
document.Recipient.map(async (recipient) => {
const { email, name, token } = recipient;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const template = createElement(DocumentCompletedEmailTemplate, {
documentName: document.title,
assetBaseUrl,
downloadLink: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${token}/complete`,
});
await mailer.sendMail({
to: {
address: email,
name,
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Signing Complete!',
html: render(template),
text: render(template, { plainText: true }),
});
}),
]);
};
@@ -0,0 +1,64 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
import { prisma } from '@documenso/prisma';
export interface SendPendingEmailOptions {
documentId: number;
recipientId: number;
}
export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingEmailOptions) => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
Recipient: {
some: {
id: recipientId,
},
},
},
include: {
Recipient: {
where: {
id: recipientId,
},
},
},
});
if (!document) {
throw new Error('Document not found');
}
if (document.Recipient.length === 0) {
throw new Error('Document has no recipients');
}
const [recipient] = document.Recipient;
const { email, name } = recipient;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const template = createElement(DocumentPendingEmailTemplate, {
documentName: document.title,
assetBaseUrl,
});
await mailer.sendMail({
to: {
address: email,
name,
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Waiting for others to complete signing.',
html: render(template),
text: render(template, { plainText: true }),
});
};
@@ -50,10 +50,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
let imageWidth = image.width;
let imageHeight = image.height;
const initialDimensions = {
width: imageWidth,
height: imageHeight,
};
// const initialDimensions = {
// width: imageWidth,
// height: imageHeight,
// };
const scalingFactor = Math.min(fieldWidth / imageWidth, fieldHeight / imageHeight, 1);
@@ -76,10 +76,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
let textWidth = font.widthOfTextAtSize(field.customText, fontSize);
const textHeight = font.heightAtSize(fontSize);
const initialDimensions = {
width: textWidth,
height: textHeight,
};
// const initialDimensions = {
// width: textWidth,
// height: textHeight,
// };
const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1);