mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Merge branch 'main' of https://github.com/documenso/documenso into feat/swagger-styling
This commit is contained in:
@ -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}` },
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
31
apps/marketing/src/components/(marketing)/call-to-action.tsx
Normal file
31
apps/marketing/src/components/(marketing)/call-to-action.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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`);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user