Merge branch 'main' of https://github.com/documenso/documenso into feat/swagger-styling

This commit is contained in:
Adithya Krishna
2024-03-13 09:56:16 +05:30
7 changed files with 215 additions and 146 deletions

View File

@ -12,6 +12,7 @@ export const BlogPost = defineDocumentType(() => ({
authorName: { type: 'string', required: true }, authorName: { type: 'string', required: true },
authorImage: { type: 'string', required: false }, authorImage: { type: 'string', required: false },
authorRole: { type: 'string', required: true }, authorRole: { type: 'string', required: true },
cta: { type: 'boolean', required: false, default: true },
}, },
computedFields: { computedFields: {
href: { type: 'string', resolve: (post) => `/${post._raw.flattenedPath}` }, href: { type: 'string', resolve: (post) => `/${post._raw.flattenedPath}` },

View File

@ -7,6 +7,8 @@ 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';
import { CallToAction } from '~/components/(marketing)/call-to-action';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export const generateMetadata = ({ params }: { params: { post: string } }) => { export const generateMetadata = ({ params }: { params: { post: string } }) => {
@ -42,6 +44,7 @@ export default function BlogPostPage({ params }: { params: { post: string } }) {
const MDXContent = useMDXComponent(post.body.code); const MDXContent = useMDXComponent(post.body.code);
return ( return (
<div>
<article className="prose dark:prose-invert mx-auto py-8"> <article className="prose dark:prose-invert mx-auto py-8">
<div className="mb-6 text-center"> <div className="mb-6 text-center">
<time dateTime={post.date} className="text-muted-foreground mb-1 text-xs"> <time dateTime={post.date} className="text-muted-foreground mb-1 text-xs">
@ -90,5 +93,8 @@ export default function BlogPostPage({ params }: { params: { post: string } }) {
Back to all posts Back to all posts
</Link> </Link>
</article> </article>
{post.cta && <CallToAction className="mt-8" utmSource={`blog__${params.post}`} />}
</div>
); );
} }

View File

@ -7,6 +7,7 @@ import { getUserMonthlyGrowth } from '@documenso/lib/server-only/user/get-user-m
import { FUNDING_RAISED } from '~/app/(marketing)/open/data'; import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
import { MetricCard } from '~/app/(marketing)/open/metric-card'; import { MetricCard } from '~/app/(marketing)/open/metric-card';
import { SalaryBands } from '~/app/(marketing)/open/salary-bands'; import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
import { CallToAction } from '~/components/(marketing)/call-to-action';
import { BarMetric } from './bar-metrics'; import { BarMetric } from './bar-metrics';
import { CapTable } from './cap-table'; import { CapTable } from './cap-table';
@ -141,6 +142,7 @@ export default async function OpenPage() {
const MONTHLY_USERS = await getUserMonthlyGrowth(); const MONTHLY_USERS = await getUserMonthlyGrowth();
return ( return (
<div>
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12"> <div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1> <h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>
@ -244,11 +246,14 @@ export default async function OpenPage() {
<h2 className="text-2xl font-bold">Where's the rest?</h2> <h2 className="text-2xl font-bold">Where's the rest?</h2>
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center text-lg leading-normal"> <p className="text-muted-foreground mt-4 max-w-[55ch] text-center text-lg leading-normal">
We're still working on getting all our metrics together. We'll update this page as soon We're still working on getting all our metrics together. We'll update this page as
as we have more to share. soon as we have more to share.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<CallToAction className="mt-12" utmSource="open-page" />
</div>
); );
} }

View File

@ -0,0 +1,31 @@
import Link from 'next/link';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
type CallToActionProps = {
className?: string;
utmSource?: string;
};
export const CallToAction = ({ className, utmSource = 'generic-cta' }: CallToActionProps) => {
return (
<Card spotlight className={className}>
<CardContent className="flex flex-col items-center justify-center p-12">
<h2 className="text-center text-2xl font-bold">Join the Open Signing Movement</h2>
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center leading-normal">
Create your account and start using state-of-the-art document signing. Open and beautiful
signing is within your grasp.
</p>
<Button className="mt-8 rounded-full no-underline" size="lg" asChild>
<Link href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=${utmSource}`} target="_blank">
Get started
</Link>
</Button>
</CardContent>
</Card>
);
};

View File

@ -1,5 +1,7 @@
'use client'; 'use client';
import { useState } from 'react';
import { signOut } from 'next-auth/react'; import { signOut } from 'next-auth/react';
import type { User } from '@documenso/prisma/client'; import type { User } from '@documenso/prisma/client';
@ -16,6 +18,8 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteAccountDialogProps = { export type DeleteAccountDialogProps = {
@ -28,6 +32,8 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
const hasTwoFactorAuthentication = user.twoFactorEnabled; const hasTwoFactorAuthentication = user.twoFactorEnabled;
const [enteredEmail, setEnteredEmail] = useState<string>('');
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } = const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
trpc.profile.deleteAccount.useMutation(); trpc.profile.deleteAccount.useMutation();
@ -76,10 +82,11 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
</div> </div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Dialog> <Dialog onOpenChange={() => setEnteredEmail('')}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="destructive">Delete Account</Button> <Button variant="destructive">Delete Account</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader className="space-y-4"> <DialogHeader className="space-y-4">
<DialogTitle>Delete Account</DialogTitle> <DialogTitle>Delete Account</DialogTitle>
@ -105,12 +112,29 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{!hasTwoFactorAuthentication && (
<div className="mt-4">
<Label>
Please type{' '}
<span className="text-muted-foreground font-semibold">{user.email}</span> to
confirm.
</Label>
<Input
type="text"
className="mt-2"
aria-label="Confirm Email"
value={enteredEmail}
onChange={(e) => setEnteredEmail(e.target.value)}
/>
</div>
)}
<DialogFooter> <DialogFooter>
<Button <Button
onClick={onDeleteAccount} onClick={onDeleteAccount}
loading={isDeletingAccount} loading={isDeletingAccount}
variant="destructive" variant="destructive"
disabled={hasTwoFactorAuthentication} disabled={hasTwoFactorAuthentication || enteredEmail !== user.email}
> >
{isDeletingAccount ? 'Deleting account...' : 'Confirm Deletion'} {isDeletingAccount ? 'Deleting account...' : 'Confirm Deletion'}
</Button> </Button>

View File

@ -16,6 +16,8 @@ test('delete user', async ({ page }) => {
}); });
await page.getByRole('button', { name: 'Delete Account' }).click(); await page.getByRole('button', { name: 'Delete Account' }).click();
await page.getByLabel('Confirm Email').fill(user.email);
await expect(page.getByRole('button', { name: 'Confirm Deletion' })).not.toBeDisabled();
await page.getByRole('button', { name: 'Confirm Deletion' }).click(); await page.getByRole('button', { name: 'Confirm Deletion' }).click();
await page.waitForURL(`${WEBAPP_BASE_URL}/signin`); await page.waitForURL(`${WEBAPP_BASE_URL}/signin`);

View File

@ -1,2 +1,2 @@
export const URL_REGEX = export const URL_REGEX =
/^(https?):\/\/(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i; /^(https?):\/\/(?:www\.)?(?:[a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i;