mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
fix: minor updates
This commit is contained in:
@ -5,6 +5,7 @@ import { Clock, File, FileCheck } from 'lucide-react';
|
|||||||
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 { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -21,6 +22,24 @@ import { LocaleDate } from '~/components/formatter/locale-date';
|
|||||||
|
|
||||||
import { UploadDocument } from './upload-document';
|
import { UploadDocument } from './upload-document';
|
||||||
|
|
||||||
|
const CARD_DATA = [
|
||||||
|
{
|
||||||
|
icon: FileCheck,
|
||||||
|
title: 'Completed',
|
||||||
|
status: InternalDocumentStatus.COMPLETED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: File,
|
||||||
|
title: 'Drafts',
|
||||||
|
status: InternalDocumentStatus.DRAFT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Clock,
|
||||||
|
title: 'Pending',
|
||||||
|
status: InternalDocumentStatus.PENDING,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const user = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
@ -34,20 +53,14 @@ export default async function DashboardPage() {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const cardData = [
|
|
||||||
{ icon: FileCheck, title: 'Completed', status: stats.COMPLETED },
|
|
||||||
{ icon: File, title: 'Drafts', status: stats.DRAFT },
|
|
||||||
{ icon: Clock, title: 'Pending', status: stats.PENDING },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<h1 className="text-4xl font-semibold">Dashboard</h1>
|
<h1 className="text-4xl font-semibold">Dashboard</h1>
|
||||||
|
|
||||||
<div className="mt-8 grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div className="mt-8 grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
{cardData.map((card) => (
|
{CARD_DATA.map((card) => (
|
||||||
<Link key={card.status} href={`/documents?status=${card.status}`}>
|
<Link key={card.status} href={`/documents?status=${card.status}`}>
|
||||||
<CardMetric icon={card.icon} title={card.title} value={card.status} />
|
<CardMetric icon={card.icon} title={card.title} value={stats[card.status]} />
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -139,9 +139,10 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state === 'signed-text' && signature?.typedSignature && (
|
{state === 'signed-text' && (
|
||||||
<p className="font-signature text-muted-foreground text-lg duration-200 sm:text-xl md:text-2xl lg:text-3xl">
|
<p className="font-signature text-muted-foreground text-lg duration-200 sm:text-xl md:text-2xl lg:text-3xl">
|
||||||
{signature.typedSignature}
|
{/* This optional chaining is intentional, we don't want to move the check into the condition above */}
|
||||||
|
{signature?.typedSignature}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Github } from 'lucide-react';
|
|
||||||
import { usePlausible } from 'next-plausible';
|
|
||||||
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
export type CalloutProps = {
|
|
||||||
starCount?: number;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Callout = ({ starCount }: CalloutProps) => {
|
|
||||||
const event = usePlausible();
|
|
||||||
|
|
||||||
const onSignUpClick = () => {
|
|
||||||
const el = document.getElementById('email');
|
|
||||||
|
|
||||||
if (el) {
|
|
||||||
const { top } = el.getBoundingClientRect();
|
|
||||||
|
|
||||||
window.scrollTo({
|
|
||||||
top: top - 120,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
el.focus();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
|
||||||
onClick={onSignUpClick}
|
|
||||||
>
|
|
||||||
Get the Community Plan
|
|
||||||
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
|
||||||
$30/mo. forever!
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href="https://github.com/documenso/documenso"
|
|
||||||
target="_blank"
|
|
||||||
onClick={() => event('view-github')}
|
|
||||||
>
|
|
||||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
|
||||||
<Github className="mr-2 h-5 w-5" />
|
|
||||||
Star on Github
|
|
||||||
{starCount && starCount > 0 && (
|
|
||||||
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
|
||||||
{starCount.toLocaleString('en-US')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,150 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { Info, Loader } from 'lucide-react';
|
|
||||||
import { usePlausible } from 'next-plausible';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} 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 { claimPlan } from '~/api/claim-plan/fetcher';
|
|
||||||
|
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
|
||||||
|
|
||||||
export const ZClaimPlanDialogFormSchema = z.object({
|
|
||||||
name: z.string().min(3),
|
|
||||||
email: z.string().email(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TClaimPlanDialogFormSchema = z.infer<typeof ZClaimPlanDialogFormSchema>;
|
|
||||||
|
|
||||||
export type ClaimPlanDialogProps = {
|
|
||||||
className?: string;
|
|
||||||
planId: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => {
|
|
||||||
const params = useSearchParams();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const event = usePlausible();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(() => params?.get('cancelled') === 'true');
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
} = useForm<TClaimPlanDialogFormSchema>({
|
|
||||||
mode: 'onBlur',
|
|
||||||
defaultValues: {
|
|
||||||
name: params?.get('name') ?? '',
|
|
||||||
email: params?.get('email') ?? '',
|
|
||||||
},
|
|
||||||
resolver: zodResolver(ZClaimPlanDialogFormSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => {
|
|
||||||
try {
|
|
||||||
const delay = new Promise<void>((resolve) => {
|
|
||||||
setTimeout(resolve, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [redirectUrl] = await Promise.all([
|
|
||||||
claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }),
|
|
||||||
delay,
|
|
||||||
]);
|
|
||||||
|
|
||||||
event('claim-plan-pricing');
|
|
||||||
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
} catch (error) {
|
|
||||||
event('claim-plan-failed');
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Something went wrong',
|
|
||||||
description: error instanceof Error ? error.message : 'Please try again later.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Claim your plan</DialogTitle>
|
|
||||||
|
|
||||||
<DialogDescription className="mt-4">
|
|
||||||
We're almost there! Please enter your email address and name to claim your plan.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<form
|
|
||||||
className={cn('flex flex-col gap-y-4', className)}
|
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
|
||||||
>
|
|
||||||
{params?.get('cancelled') === 'true' && (
|
|
||||||
<div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<Info className="h-5 w-5 text-yellow-400" />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<p className="text-sm leading-5 text-yellow-700">
|
|
||||||
You have cancelled the payment process. If you didn't mean to do this, please
|
|
||||||
try again.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label className="text-slate-500">Name</Label>
|
|
||||||
|
|
||||||
<Input type="text" className="mt-2" {...register('name')} autoFocus />
|
|
||||||
|
|
||||||
<FormErrorMessage className="mt-1" error={errors.name} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label className="text-slate-500">Email</Label>
|
|
||||||
|
|
||||||
<Input type="email" className="mt-2" {...register('email')} />
|
|
||||||
|
|
||||||
<FormErrorMessage className="mt-1" error={errors.email} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" size="lg" disabled={isSubmitting}>
|
|
||||||
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
|
|
||||||
Claim the Community Plan ({/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
|
|
||||||
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
|
||||||
? 'Monthly'
|
|
||||||
: 'Yearly'}
|
|
||||||
)
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
|
|
||||||
import backgroundPattern from '~/assets/background-pattern.png';
|
|
||||||
import cardBeautifulFigure from '~/assets/card-beautiful-figure.png';
|
|
||||||
import cardFastFigure from '~/assets/card-fast-figure.png';
|
|
||||||
import cardSmartFigure from '~/assets/card-smart-figure.png';
|
|
||||||
|
|
||||||
export type FasterSmarterBeautifulBentoProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const FasterSmarterBeautifulBento = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: FasterSmarterBeautifulBentoProps) => {
|
|
||||||
return (
|
|
||||||
<div className={cn('relative', className)} {...props}>
|
|
||||||
<div className="absolute inset-0 -z-10 flex items-center justify-center">
|
|
||||||
<Image
|
|
||||||
src={backgroundPattern}
|
|
||||||
alt="background pattern"
|
|
||||||
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
|
|
||||||
A 10x better signing experience.
|
|
||||||
<span className="block md:mt-0">Faster, smarter and more beautiful.</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
|
|
||||||
<Card className="col-span-2" degrees={45} gradient>
|
|
||||||
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
|
|
||||||
<p className="col-span-12 leading-relaxed text-[#555E67] lg:col-span-6">
|
|
||||||
<strong className="block">Fast.</strong>
|
|
||||||
When it comes to sending or receiving a contract, you can count on lightning-fast
|
|
||||||
speeds.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
|
|
||||||
<Image src={cardFastFigure} alt="its fast" className="max-w-[80%] lg:max-w-none" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
|
||||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
|
||||||
<p className="leading-relaxed text-[#555E67]">
|
|
||||||
<strong className="block">Beautiful.</strong>
|
|
||||||
Because signing should be celebrated. That’s why we care about the smallest detail in
|
|
||||||
our product.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<Image src={cardBeautifulFigure} alt="its fast" className="w-full max-w-xs" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
|
||||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
|
||||||
<p className="leading-relaxed text-[#555E67]">
|
|
||||||
<strong className="block">Smart.</strong>
|
|
||||||
Our custom templates come with smart rules that can help you save time and energy.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<Image src={cardSmartFigure} alt="its fast" className="w-full max-w-[16rem]" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Github, MessagesSquare, Twitter } from 'lucide-react';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
const SOCIAL_LINKS = [
|
|
||||||
{ href: 'https://twitter.com/documenso', icon: <Twitter className="h-6 w-6" /> },
|
|
||||||
{ href: 'https://github.com/documenso/documenso', icon: <Github className="h-6 w-6" /> },
|
|
||||||
{ href: 'https://documen.so/discord', icon: <MessagesSquare className="h-6 w-6" /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
const FOOTER_LINKS = [
|
|
||||||
{ href: '/pricing', text: 'Pricing' },
|
|
||||||
{ href: 'https://status.documenso.com', text: 'Status', target: '_blank' },
|
|
||||||
{ href: 'mailto:support@documenso.com', text: 'Support' },
|
|
||||||
// { href: '/privacy', text: 'Privacy'}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Footer = ({ className, ...props }: FooterProps) => {
|
|
||||||
return (
|
|
||||||
<div className={cn('border-t py-12', className)} {...props}>
|
|
||||||
<div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8">
|
|
||||||
<div>
|
|
||||||
<Link href="/">
|
|
||||||
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4 text-[#8D8D8D]">
|
|
||||||
{SOCIAL_LINKS.map((link, index) => (
|
|
||||||
<Link key={index} href={link.href} target="_blank" className="hover:text-[#6D6D6D]">
|
|
||||||
{link.icon}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2.5">
|
|
||||||
{FOOTER_LINKS.map((link, index) => (
|
|
||||||
<Link
|
|
||||||
key={index}
|
|
||||||
href={link.href}
|
|
||||||
target={link.target}
|
|
||||||
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
|
|
||||||
>
|
|
||||||
{link.text}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto mt-4 w-full max-w-screen-xl px-8 md:mt-12 lg:mt-24">
|
|
||||||
<p className="text-sm text-[#8D8D8D]">
|
|
||||||
© {new Date().getFullYear()} Documenso, Inc. All rights reserved.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
export type HeaderProps = HTMLAttributes<HTMLElement>;
|
|
||||||
|
|
||||||
export const Header = ({ className, ...props }: HeaderProps) => {
|
|
||||||
return (
|
|
||||||
<header className={cn('flex items-center justify-between', className)} {...props}>
|
|
||||||
<Link href="/">
|
|
||||||
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-x-6">
|
|
||||||
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
|
|
||||||
Pricing
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href="https://app.documenso.com/login"
|
|
||||||
target="_blank"
|
|
||||||
className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,225 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Variants, motion } from 'framer-motion';
|
|
||||||
import { Github } from 'lucide-react';
|
|
||||||
import { usePlausible } from 'next-plausible';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import backgroundPattern from '~/assets/background-pattern.png';
|
|
||||||
|
|
||||||
import { Widget } from './widget';
|
|
||||||
|
|
||||||
export type HeroProps = {
|
|
||||||
className?: string;
|
|
||||||
starCount?: number;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BackgroundPatternVariants: Variants = {
|
|
||||||
initial: {
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
animate: {
|
|
||||||
opacity: 1,
|
|
||||||
|
|
||||||
transition: {
|
|
||||||
delay: 1,
|
|
||||||
duration: 1.2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const HeroTitleVariants: Variants = {
|
|
||||||
initial: {
|
|
||||||
opacity: 0,
|
|
||||||
y: 60,
|
|
||||||
},
|
|
||||||
animate: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: {
|
|
||||||
duration: 0.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Hero = ({ className, starCount, ...props }: HeroProps) => {
|
|
||||||
const event = usePlausible();
|
|
||||||
|
|
||||||
const onSignUpClick = () => {
|
|
||||||
const el = document.getElementById('email');
|
|
||||||
|
|
||||||
if (el) {
|
|
||||||
const { top } = el.getBoundingClientRect();
|
|
||||||
|
|
||||||
window.scrollTo({
|
|
||||||
top: top - 120,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
el.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div className={cn('relative', className)} {...props}>
|
|
||||||
<div className="absolute -inset-24 -z-10">
|
|
||||||
<motion.div
|
|
||||||
className="flex h-full w-full origin-top-right items-center justify-center"
|
|
||||||
variants={BackgroundPatternVariants}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={backgroundPattern}
|
|
||||||
alt="background pattern"
|
|
||||||
className="-mr-[50vw] -mt-[15vh] h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<motion.h2
|
|
||||||
variants={HeroTitleVariants}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
className="text-center text-4xl font-bold leading-tight tracking-tight lg:text-[64px]"
|
|
||||||
>
|
|
||||||
Document signing,
|
|
||||||
<span className="block" /> finally open source.
|
|
||||||
</motion.h2>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
variants={HeroTitleVariants}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-4"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
|
||||||
onClick={onSignUpClick}
|
|
||||||
>
|
|
||||||
Get the Community Plan
|
|
||||||
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
|
||||||
$30/mo. forever!
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
|
|
||||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
|
||||||
<Github className="mr-2 h-5 w-5" />
|
|
||||||
Star on Github
|
|
||||||
{starCount && starCount > 0 && (
|
|
||||||
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
|
||||||
{starCount.toLocaleString('en-US')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-6">
|
|
||||||
<motion.div
|
|
||||||
variants={HeroTitleVariants}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-4"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily"
|
|
||||||
alt="Documenso - The open source DocuSign alternative | Product Hunt"
|
|
||||||
style={{ width: '250px', height: '54px' }}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="mt-12"
|
|
||||||
variants={{
|
|
||||||
initial: {
|
|
||||||
scale: 0.2,
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
animate: {
|
|
||||||
scale: 1,
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
ease: 'easeInOut',
|
|
||||||
delay: 0.5,
|
|
||||||
duration: 0.8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
>
|
|
||||||
<Widget className="mt-12">
|
|
||||||
<strong>Documenso Supporter Pledge</strong>
|
|
||||||
<p className="w-full max-w-[70ch]">
|
|
||||||
Our mission is to create an open signing infrastructure that empowers the world,
|
|
||||||
enabling businesses to embrace openness, cooperation, and transparency. We believe
|
|
||||||
that signing, as a fundamental act, should embody these values. By offering an
|
|
||||||
open-source signing solution, we aim to make document signing accessible, transparent,
|
|
||||||
and trustworthy.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="w-full max-w-[70ch]">
|
|
||||||
Through our platform, called Documenso, we strive to earn your trust by allowing
|
|
||||||
self-hosting and providing complete visibility into its inner workings. We value
|
|
||||||
inclusivity and foster an environment where diverse perspectives and contributions are
|
|
||||||
welcomed, even though we may not implement them all.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="w-full max-w-[70ch]">
|
|
||||||
At Documenso, we envision a web-enabled future for business and contracts, and we are
|
|
||||||
committed to being the leading provider of open signing infrastructure. By combining
|
|
||||||
exceptional product design with open-source principles, we aim to deliver a robust and
|
|
||||||
well-designed application that exceeds your expectations.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="w-full max-w-[70ch]">
|
|
||||||
We understand that exceptional products are born from exceptional communities, and we
|
|
||||||
invite you to join our open-source community. Your contributions, whether technical or
|
|
||||||
non-technical, will help shape the future of signing. Together, we can create a better
|
|
||||||
future for everyone.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="w-full max-w-[70ch]">
|
|
||||||
Today we invite you to join us on this journey: By signing this mission statement you
|
|
||||||
signal your support of Documenso's mission{' '}
|
|
||||||
<span className="bg-primary text-black">
|
|
||||||
(in a non-legally binding, but heartfelt way)
|
|
||||||
</span>{' '}
|
|
||||||
and lock in the early supporter plan for forever, including everything we build this
|
|
||||||
year.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex h-24 items-center">
|
|
||||||
<p className={cn('text-5xl [font-family:var(--font-caveat)]')}>Timur & Lucas</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<strong>Timur Ercan & Lucas Smith</strong>
|
|
||||||
<p className="mt-1">Co-Founders, Documenso</p>
|
|
||||||
</div>
|
|
||||||
</Widget>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
|
|
||||||
import backgroundPattern from '~/assets/background-pattern.png';
|
|
||||||
import cardBuildFigure from '~/assets/card-build-figure.png';
|
|
||||||
import cardOpenFigure from '~/assets/card-open-figure.png';
|
|
||||||
import cardTemplateFigure from '~/assets/card-template-figure.png';
|
|
||||||
|
|
||||||
export type OpenBuildTemplateBentoProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplateBentoProps) => {
|
|
||||||
return (
|
|
||||||
<div className={cn('relative', className)} {...props}>
|
|
||||||
<div className="absolute inset-0 -z-10 flex items-center justify-center">
|
|
||||||
<Image
|
|
||||||
src={backgroundPattern}
|
|
||||||
alt="background pattern"
|
|
||||||
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
|
|
||||||
Truly your own.
|
|
||||||
<span className="block md:mt-0">Customise and expand.</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
|
|
||||||
<Card className="col-span-2" degrees={45} gradient>
|
|
||||||
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
|
|
||||||
<p className="col-span-12 leading-relaxed text-[#555E67] lg:col-span-6">
|
|
||||||
<strong className="block">Open Source or Hosted.</strong>
|
|
||||||
It’s up to you. Either clone our repository or rely on our easy to use hosting
|
|
||||||
solution.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
|
|
||||||
<Image src={cardOpenFigure} alt="its fast" className="max-w-[80%] lg:max-w-full" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
|
||||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
|
||||||
<p className="leading-relaxed text-[#555E67]">
|
|
||||||
<strong className="block">Build on top.</strong>
|
|
||||||
Make it your own through advanced customization and adjustability.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<Image src={cardBuildFigure} alt="its fast" className="w-full max-w-xs" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
|
||||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
|
||||||
<p className="leading-relaxed text-[#555E67]">
|
|
||||||
<strong className="block">Template Store (Soon).</strong>
|
|
||||||
Choose a template from the community app store. Or submit your own template for others
|
|
||||||
to use.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<Image src={cardTemplateFigure} alt="its fast" className="w-full max-w-sm" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
|
|
||||||
|
|
||||||
export type PasswordRevealProps = {
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PasswordReveal = ({ password }: PasswordRevealProps) => {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [, copy] = useCopyToClipboard();
|
|
||||||
|
|
||||||
const onCopyClick = () => {
|
|
||||||
void copy(password).then(() => {
|
|
||||||
toast({
|
|
||||||
title: 'Copied to clipboard',
|
|
||||||
description: 'Your password has been copied to your clipboard.',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="px-2 blur-sm hover:opacity-50 hover:blur-none"
|
|
||||||
onClick={onCopyClick}
|
|
||||||
>
|
|
||||||
{password}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,180 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { HTMLAttributes, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import { usePlausible } from 'next-plausible';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import { ClaimPlanDialog } from './claim-plan-dialog';
|
|
||||||
|
|
||||||
export type PricingTableProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
const SELECTED_PLAN_BAR_LAYOUT_ID = 'selected-plan-bar';
|
|
||||||
|
|
||||||
export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
|
||||||
const params = useSearchParams();
|
|
||||||
const event = usePlausible();
|
|
||||||
|
|
||||||
const [period, setPeriod] = useState<'MONTHLY' | 'YEARLY'>(() =>
|
|
||||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
||||||
params?.get('planId') === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID
|
|
||||||
? 'YEARLY'
|
|
||||||
: 'MONTHLY',
|
|
||||||
);
|
|
||||||
|
|
||||||
const planId = useMemo(() => {
|
|
||||||
if (period === 'MONTHLY') {
|
|
||||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
||||||
return process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
||||||
return process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID;
|
|
||||||
}, [period]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('', className)} {...props}>
|
|
||||||
<div className="flex items-center justify-center gap-x-6">
|
|
||||||
<AnimatePresence>
|
|
||||||
<motion.button
|
|
||||||
key="MONTHLY"
|
|
||||||
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', {
|
|
||||||
'text-slate-900': period === 'MONTHLY',
|
|
||||||
'hover:text-slate-900/80': period !== 'MONTHLY',
|
|
||||||
})}
|
|
||||||
onClick={() => setPeriod('MONTHLY')}
|
|
||||||
>
|
|
||||||
Monthly
|
|
||||||
{period === 'MONTHLY' && (
|
|
||||||
<motion.div
|
|
||||||
layoutId={SELECTED_PLAN_BAR_LAYOUT_ID}
|
|
||||||
className="bg-primary absolute bottom-0 left-0 h-[3px] w-full rounded-full"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</motion.button>
|
|
||||||
|
|
||||||
<motion.button
|
|
||||||
key="YEARLY"
|
|
||||||
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', {
|
|
||||||
'text-slate-900': period === 'YEARLY',
|
|
||||||
'hover:text-slate-900/80': period !== 'YEARLY',
|
|
||||||
})}
|
|
||||||
onClick={() => setPeriod('YEARLY')}
|
|
||||||
>
|
|
||||||
Yearly
|
|
||||||
<div className="block rounded-full bg-slate-200 px-2 py-0.5 text-xs text-slate-700">
|
|
||||||
Save $60
|
|
||||||
</div>
|
|
||||||
{period === 'YEARLY' && (
|
|
||||||
<motion.div
|
|
||||||
layoutId={SELECTED_PLAN_BAR_LAYOUT_ID}
|
|
||||||
className="bg-primary absolute bottom-0 left-0 h-[3px] w-full rounded-full"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</motion.button>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
<div
|
|
||||||
data-plan="self-hosted"
|
|
||||||
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5"
|
|
||||||
>
|
|
||||||
<p className="text-4xl font-medium text-slate-900">Self Hosted</p>
|
|
||||||
<p className="text-primary mt-2.5 text-xl font-medium">Free</p>
|
|
||||||
|
|
||||||
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
|
|
||||||
For small teams and individuals who need a simple solution
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Button className="mt-6 rounded-full text-base">
|
|
||||||
<Link
|
|
||||||
href="https://github.com/documenso/documenso"
|
|
||||||
target="_blank"
|
|
||||||
onClick={() => event('view-github')}
|
|
||||||
>
|
|
||||||
View on Github
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-col divide-y">
|
|
||||||
<p className="py-4 font-medium text-slate-900">Host your own instance</p>
|
|
||||||
<p className="py-4 text-slate-900">Full Control</p>
|
|
||||||
<p className="py-4 text-slate-900">Customizability</p>
|
|
||||||
<p className="py-4 text-slate-900">Docker Ready</p>
|
|
||||||
<p className="py-4 text-slate-900">Community Support</p>
|
|
||||||
<p className="py-4 text-slate-900">Free, Forever</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
data-plan="community"
|
|
||||||
className="border-primary flex flex-col items-center justify-center rounded-lg border-2 bg-white px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380] shadow-slate-900/5"
|
|
||||||
>
|
|
||||||
<p className="text-4xl font-medium text-slate-900">Community</p>
|
|
||||||
<div className="text-primary mt-2.5 text-xl font-medium">
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
|
|
||||||
{period === 'YEARLY' && <motion.div layoutId="pricing">$300</motion.div>}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
|
|
||||||
For fast-growing companies that aim to scale across multiple teams.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ClaimPlanDialog planId={planId}>
|
|
||||||
<Button className="mt-6 rounded-full text-base">Signup Now</Button>
|
|
||||||
</ClaimPlanDialog>
|
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-col divide-y">
|
|
||||||
<p className="py-4 font-medium text-slate-900">Documenso Early Adopter Deal:</p>
|
|
||||||
<p className="py-4 text-slate-900">Join the movement</p>
|
|
||||||
<p className="py-4 text-slate-900">Simple signing solution</p>
|
|
||||||
<p className="py-4 text-slate-900">Email and Slack assistance</p>
|
|
||||||
<p className="py-4 text-slate-900">
|
|
||||||
<strong>Includes all upcoming features</strong>
|
|
||||||
</p>
|
|
||||||
<p className="py-4 text-slate-900">Fixed, straightforward pricing</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
data-plan="enterprise"
|
|
||||||
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5"
|
|
||||||
>
|
|
||||||
<p className="text-4xl font-medium text-slate-900">Enterprise</p>
|
|
||||||
<p className="text-primary mt-2.5 text-xl font-medium">Pricing on request</p>
|
|
||||||
|
|
||||||
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
|
|
||||||
For large organizations that need extra flexibility and control.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href="https://dub.sh/enterprise"
|
|
||||||
target="_blank"
|
|
||||||
className="mt-6"
|
|
||||||
onClick={() => event('enterprise-contact')}
|
|
||||||
>
|
|
||||||
<Button className="rounded-full text-base">Contact Us</Button>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-col divide-y">
|
|
||||||
<p className="py-4 font-medium text-slate-900">Everything in Community, plus:</p>
|
|
||||||
<p className="py-4 text-slate-900">Custom Subdomain</p>
|
|
||||||
<p className="py-4 text-slate-900">Compliance Check</p>
|
|
||||||
<p className="py-4 text-slate-900">Guaranteed Uptime</p>
|
|
||||||
<p className="py-4 text-slate-900">Reporting & Analysis</p>
|
|
||||||
<p className="py-4 text-slate-900">24/7 Support</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
|
|
||||||
import backgroundPattern from '~/assets/background-pattern.png';
|
|
||||||
import cardConnectionsFigure from '~/assets/card-connections-figure.png';
|
|
||||||
import cardPaidFigure from '~/assets/card-paid-figure.png';
|
|
||||||
import cardSharingFigure from '~/assets/card-sharing-figure.png';
|
|
||||||
import cardWidgetFigure from '~/assets/card-widget-figure.png';
|
|
||||||
|
|
||||||
export type ShareConnectPaidWidgetBentoProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const ShareConnectPaidWidgetBento = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ShareConnectPaidWidgetBentoProps) => {
|
|
||||||
return (
|
|
||||||
<div className={cn('relative', className)} {...props}>
|
|
||||||
<div className="absolute inset-0 -z-10 flex items-center justify-center">
|
|
||||||
<Image
|
|
||||||
src={backgroundPattern}
|
|
||||||
alt="background pattern"
|
|
||||||
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
|
|
||||||
Integrates with all your favourite tools.
|
|
||||||
<span className="block md:mt-0">Send, connect, receive and embed everywhere.</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
|
|
||||||
<Card className="col-span-2 lg:col-span-1" degrees={120} gradient>
|
|
||||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
|
||||||
<p className="leading-relaxed text-[#555E67]">
|
|
||||||
<strong className="block">Easy Sharing (Soon).</strong>
|
|
||||||
Receive your personal link to share with everyone you care about.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<Image src={cardSharingFigure} alt="its fast" className="w-full max-w-xs" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
|
||||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
|
||||||
<p className="leading-relaxed text-[#555E67]">
|
|
||||||
<strong className="block">Connections (Soon).</strong>
|
|
||||||
Create connections and automations with Zapier and more to integrate with your
|
|
||||||
favorite tools.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<Image src={cardConnectionsFigure} alt="its fast" className="w-full max-w-sm" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
|
||||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
|
||||||
<p className="leading-relaxed text-[#555E67]">
|
|
||||||
<strong className="block">Get paid (Soon).</strong>
|
|
||||||
Integrated payments with stripe so you don’t have to worry about getting paid.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<Image src={cardPaidFigure} alt="its fast" className="w-full max-w-[14rem]" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="col-span-2 lg:col-span-1" spotlight>
|
|
||||||
<CardContent className="grid grid-cols-1 gap-8 p-6">
|
|
||||||
<p className="leading-relaxed text-[#555E67]">
|
|
||||||
<strong className="block">React Widget (Soon).</strong>
|
|
||||||
Easily embed Documenso into your product. Simply copy and paste our react widget into
|
|
||||||
your application.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<Image src={cardWidgetFigure} alt="its fast" className="w-full max-w-xs" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,402 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { HTMLAttributes, KeyboardEvent, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import { Loader } from 'lucide-react';
|
|
||||||
import { usePlausible } from 'next-plausible';
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@documenso/ui/primitives/dialog';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { claimPlan } from '~/api/claim-plan/fetcher';
|
|
||||||
|
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
|
||||||
|
|
||||||
const ZWidgetFormSchema = z
|
|
||||||
.object({
|
|
||||||
email: z.string().email({ message: 'Please enter a valid email address.' }),
|
|
||||||
name: z.string().min(3, { message: 'Please enter a valid name.' }),
|
|
||||||
})
|
|
||||||
.and(
|
|
||||||
z.union([
|
|
||||||
z.object({
|
|
||||||
signatureDataUrl: z.string().min(1),
|
|
||||||
signatureText: z.null().or(z.string().max(0)),
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
signatureDataUrl: z.null().or(z.string().max(0)),
|
|
||||||
signatureText: z.string().min(1),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TWidgetFormSchema = z.infer<typeof ZWidgetFormSchema>;
|
|
||||||
|
|
||||||
export type WidgetProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const event = usePlausible();
|
|
||||||
|
|
||||||
const [step, setStep] = useState<'EMAIL' | 'NAME' | 'SIGN'>('EMAIL');
|
|
||||||
const [showSigningDialog, setShowSigningDialog] = useState(false);
|
|
||||||
const [draftSignatureDataUrl, setDraftSignatureDataUrl] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
trigger,
|
|
||||||
watch,
|
|
||||||
formState: { errors, isSubmitting, isValid },
|
|
||||||
} = useForm<TWidgetFormSchema>({
|
|
||||||
mode: 'onChange',
|
|
||||||
defaultValues: {
|
|
||||||
email: '',
|
|
||||||
name: '',
|
|
||||||
signatureDataUrl: null,
|
|
||||||
signatureText: '',
|
|
||||||
},
|
|
||||||
resolver: zodResolver(ZWidgetFormSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const signatureDataUrl = watch('signatureDataUrl');
|
|
||||||
const signatureText = watch('signatureText');
|
|
||||||
|
|
||||||
const stepsRemaining = useMemo(() => {
|
|
||||||
if (step === 'NAME') {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === 'SIGN') {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 3;
|
|
||||||
}, [step]);
|
|
||||||
|
|
||||||
const onNextStepClick = () => {
|
|
||||||
if (step === 'EMAIL') {
|
|
||||||
setStep('NAME');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
document.querySelector<HTMLElement>('#name')?.focus();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === 'NAME') {
|
|
||||||
setStep('SIGN');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
document.querySelector<HTMLElement>('#signatureText')?.focus();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onEnterPress = (callback: () => void) => {
|
|
||||||
return (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSignatureConfirmClick = () => {
|
|
||||||
setValue('signatureDataUrl', draftSignatureDataUrl);
|
|
||||||
setValue('signatureText', '');
|
|
||||||
|
|
||||||
void trigger('signatureDataUrl');
|
|
||||||
setShowSigningDialog(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
signatureDataUrl,
|
|
||||||
signatureText,
|
|
||||||
}: TWidgetFormSchema) => {
|
|
||||||
try {
|
|
||||||
const delay = new Promise<void>((resolve) => {
|
|
||||||
setTimeout(resolve, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
||||||
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
|
|
||||||
|
|
||||||
const claimPlanInput = signatureDataUrl
|
|
||||||
? {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
planId,
|
|
||||||
signatureDataUrl: signatureDataUrl!,
|
|
||||||
signatureText: null,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
planId,
|
|
||||||
signatureDataUrl: null,
|
|
||||||
signatureText: signatureText!,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [result] = await Promise.all([claimPlan(claimPlanInput), delay]);
|
|
||||||
|
|
||||||
event('claim-plan-widget');
|
|
||||||
|
|
||||||
window.location.href = result;
|
|
||||||
} catch (error) {
|
|
||||||
event('claim-plan-failed');
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Something went wrong',
|
|
||||||
description: error instanceof Error ? error.message : 'Please try again later.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card
|
|
||||||
className={cn('mx-auto w-full max-w-4xl rounded-3xl before:rounded-3xl', className)}
|
|
||||||
gradient
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8">
|
|
||||||
<div className="col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed text-[#727272] lg:col-span-7">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
className="col-span-12 flex flex-col rounded-2xl bg-[#F7F7F7] p-6 lg:col-span-5"
|
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
|
||||||
>
|
|
||||||
<h3 className="text-2xl font-semibold">Sign up for the community plan</h3>
|
|
||||||
<p className="mt-2 text-xs text-[#AFAFAF]">
|
|
||||||
with Timur Ercan & Lucas Smith from Documenso
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<hr className="mb-6 mt-4" />
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
<motion.div key="email">
|
|
||||||
<label htmlFor="email" className="text-lg font-semibold text-slate-900 lg:text-xl">
|
|
||||||
What’s your email?
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="relative mt-2">
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
placeholder=""
|
|
||||||
className="w-full bg-white pr-16"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
onKeyDown={(e) =>
|
|
||||||
field.value !== '' &&
|
|
||||||
!errors.email?.message &&
|
|
||||||
onEnterPress(onNextStepClick)(e)
|
|
||||||
}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="absolute inset-y-0 right-0 p-1.5">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="bg-primary h-full w-14 rounded"
|
|
||||||
disabled={!field.value || !!errors.email?.message}
|
|
||||||
onClick={() => onNextStepClick()}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormErrorMessage error={errors.email} className="mt-1" />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{(step === 'NAME' || step === 'SIGN') && (
|
|
||||||
<motion.div
|
|
||||||
key="name"
|
|
||||||
className="mt-4"
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
transform: 'translateX(0)',
|
|
||||||
}}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
transform: 'translateX(-25%)',
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
transform: 'translateX(25%)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label htmlFor="name" className="text-lg font-semibold text-slate-900 lg:text-xl">
|
|
||||||
and your name?
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="relative mt-2">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
type="text"
|
|
||||||
placeholder=""
|
|
||||||
className="w-full bg-white pr-16"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
onKeyDown={(e) =>
|
|
||||||
field.value !== '' &&
|
|
||||||
!errors.name?.message &&
|
|
||||||
onEnterPress(onNextStepClick)(e)
|
|
||||||
}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="absolute inset-y-0 right-0 p-1.5">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="bg-primary h-full w-14 rounded"
|
|
||||||
disabled={!field.value || !!errors.name?.message}
|
|
||||||
onClick={() => onNextStepClick()}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormErrorMessage error={errors.name} className="mt-1" />
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<div className="mt-12 flex-1" />
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-xs text-[#AFAFAF]">{stepsRemaining} step(s) until signed</p>
|
|
||||||
<p className="block text-xs text-[#AFAFAF] md:hidden">Minimise contract</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative mt-2.5 h-[2px] w-full bg-[#E9E9E9]">
|
|
||||||
<div
|
|
||||||
className={cn('bg-primary/60 absolute inset-y-0 left-0 duration-200', {
|
|
||||||
'w-1/3': stepsRemaining === 3,
|
|
||||||
'w-2/3': stepsRemaining === 2,
|
|
||||||
'w-11/12': stepsRemaining === 1,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card id="signature" className="mt-4" degrees={-140} gradient>
|
|
||||||
<CardContent
|
|
||||||
role="button"
|
|
||||||
className="relative cursor-pointer pt-6"
|
|
||||||
onClick={() => setShowSigningDialog(true)}
|
|
||||||
>
|
|
||||||
<div className="flex h-28 items-center justify-center pb-6">
|
|
||||||
{!signatureText && signatureDataUrl && (
|
|
||||||
<img src={signatureDataUrl} alt="user signature" className="h-full" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{signatureText && (
|
|
||||||
<p
|
|
||||||
className={cn(
|
|
||||||
'text-4xl font-semibold text-slate-900 [font-family:var(--font-caveat)]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{signatureText}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="absolute inset-x-0 bottom-0 flex cursor-auto items-center justify-between px-4 pb-2"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
id="signatureText"
|
|
||||||
className="border-none p-0 text-sm text-slate-700 placeholder:text-[#D6D6D6] focus-visible:ring-0"
|
|
||||||
placeholder="Draw or type name here"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
{...register('signatureText', {
|
|
||||||
onChange: (e) => {
|
|
||||||
if (e.target.value !== '') {
|
|
||||||
setValue('signatureDataUrl', null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="h-8 disabled:bg-[#ECEEED] disabled:text-[#C6C6C6] disabled:hover:bg-[#ECEEED]"
|
|
||||||
disabled={!isValid || isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
|
|
||||||
Sign
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Dialog open={showSigningDialog} onOpenChange={setShowSigningDialog}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Add your signature</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<DialogDescription>
|
|
||||||
By signing you signal your support of Documenso's mission in a <br></br>
|
|
||||||
<strong>non-legally binding, but heartfelt way</strong>. <br></br>
|
|
||||||
<br></br>You also unlock the option to purchase the early supporter plan including
|
|
||||||
everything we build this year for fixed price.
|
|
||||||
</DialogDescription>
|
|
||||||
|
|
||||||
<SignaturePad
|
|
||||||
className="aspect-video w-full rounded-md border"
|
|
||||||
onChange={setDraftSignatureDataUrl}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="ghost" onClick={() => setShowSigningDialog(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button onClick={() => onSignatureConfirmClick()}>Confirm</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user