mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
6 Commits
v1.5.4-rc.
...
feat/publi
| Author | SHA1 | Date | |
|---|---|---|---|
| 06aae9abd0 | |||
| dfb1e8a121 | |||
| 90b9b58afe | |||
| 7e9efe9139 | |||
| 767679cdc6 | |||
| 0664de93ab |
@ -2,9 +2,12 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-lw-2.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { AnnouncementBar } from '@documenso/ui/primitives/announcement-bar';
|
||||
|
||||
import { Footer } from '~/components/(marketing)/footer';
|
||||
import { Header } from '~/components/(marketing)/header';
|
||||
@ -38,6 +41,14 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
||||
'bg-background/50 backdrop-blur-md': scrollY > 5,
|
||||
})}
|
||||
>
|
||||
<div className="absolute -inset-0 -z-[1] opacity-100">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="h-[2rem] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<AnnouncementBar className="relative" isShown={true} />
|
||||
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
</div>
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ export default function NotFound() {
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="secondary"
|
||||
className="w-32"
|
||||
onClick={() => {
|
||||
void router.back();
|
||||
|
||||
@ -40,9 +40,9 @@ export const Callout = ({ starCount }: CalloutProps) => {
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Get the Early Adopters Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
$30/mo. forever!
|
||||
Claim Community Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
-80%
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
@ -74,6 +74,15 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="https://app.documenso.com/signin"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
<span className="bg-primary dark:text-background rounded-full px-3 py-2 text-xs">
|
||||
Sign up
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<HamburgerMenu
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Variants, motion } from 'framer-motion';
|
||||
import type { Variants } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { LuGithub } from 'react-icons/lu';
|
||||
import { match } from 'ts-pattern';
|
||||
@ -113,9 +114,9 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Get the Early Adopters Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
$30/mo. forever!
|
||||
Claim Community Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
-80%
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
@ -190,42 +191,32 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
<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.
|
||||
Our mission is to create an open signing infrastructure that empowers the world. We
|
||||
believe openness and cooperation are the way every business should be conducted.
|
||||
</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.
|
||||
By creating an open source signing solution we want to bring these values to
|
||||
businesses' most fundamental act: signing. Document Signing should be open and
|
||||
transparent, as should all trust based products.
|
||||
</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.
|
||||
We aim to earn this trust by enabling everyone to self-host Documenso and inspect it’s
|
||||
inner workings. We openly share our source, knowledge, and progress while creating
|
||||
Documenso.
|
||||
</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.
|
||||
Exceptional products are the results of exceptional communities and we strive to
|
||||
create an inclusive, creative environment, open to all who choose to support our
|
||||
mission. We value the inputs, contributions, and perspectives of everyone in our
|
||||
community, even though we can't apply them all.
|
||||
</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.
|
||||
We are building the next generation of trust software and community the way it’s meant
|
||||
to be: Beautifully designed and open for all to join.
|
||||
</p>
|
||||
|
||||
<div className="flex h-24 items-center">
|
||||
|
||||
@ -50,6 +50,10 @@ export const MENU_NAVIGATION_LINKS = [
|
||||
href: 'https://app.documenso.com/signin',
|
||||
text: 'Sign in',
|
||||
},
|
||||
{
|
||||
href: 'https://app.documenso.com/signup',
|
||||
text: 'Sign up',
|
||||
},
|
||||
];
|
||||
|
||||
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
||||
@ -104,7 +108,13 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
||||
onClick={() => handleMenuItemClick()}
|
||||
target={target}
|
||||
>
|
||||
{text}
|
||||
{href === 'https://app.documenso.com/signup' ? (
|
||||
<span className="bg-primary dark:text-background rounded-full px-3 py-2 text-xl">
|
||||
{text}
|
||||
</span>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
@ -194,7 +194,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
||||
className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<h3 className="text-2xl font-semibold">Sign up for the early adopters plan</h3>
|
||||
<h3 className="text-xl font-semibold">Sign up to Community Plan</h3>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
with Timur Ercan & Lucas Smith from Documenso
|
||||
</p>
|
||||
@ -215,7 +215,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder=""
|
||||
placeholder="your@example.com"
|
||||
className="bg-background w-full pr-16"
|
||||
disabled={isSubmitting}
|
||||
onKeyDown={(e) =>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
|
||||
import type { DocumentsPageViewProps } from './documents-page-view';
|
||||
import { DocumentsPageView } from './documents-page-view';
|
||||
import { PublicProfileIntro } from './username-claim/public-profile-intro';
|
||||
|
||||
export type DocumentsPageProps = {
|
||||
searchParams?: DocumentsPageViewProps['searchParams'];
|
||||
@ -11,6 +14,12 @@ export const metadata: Metadata = {
|
||||
title: 'Documents',
|
||||
};
|
||||
|
||||
export default function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||
return <DocumentsPageView searchParams={searchParams} />;
|
||||
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
return (
|
||||
<>
|
||||
<PublicProfileIntro user={user} />
|
||||
<DocumentsPageView searchParams={searchParams} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,232 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { File } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import Check from '@documenso/assets/Check.svg';
|
||||
import Lucas from '@documenso/assets/images/Lucas.png';
|
||||
import Timur from '@documenso/assets/images/Timur.png';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { TRPCClientError } from '@documenso/trpc/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardHeader } from '@documenso/ui/primitives/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export const ZPublicProfileFormSchema = z.object({
|
||||
profileURL: z.string().trim().min(1, { message: 'Please enter a valid URL slug.' }),
|
||||
});
|
||||
|
||||
export type TPublicProfileFormSchema = z.infer<typeof ZPublicProfileFormSchema>;
|
||||
|
||||
export type PublicProfileIntroProps = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const PublicProfileIntro = ({ user }: PublicProfileIntroProps) => {
|
||||
const form = useForm<TPublicProfileFormSchema>({
|
||||
values: {
|
||||
profileURL: user.profileURL || '',
|
||||
},
|
||||
resolver: zodResolver(ZPublicProfileFormSchema),
|
||||
});
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync: updatePublicProfile } = trpc.profile.updatePublicProfile.useMutation();
|
||||
const isSaving = form.formState.isSubmitting;
|
||||
|
||||
const isProfileURLClaimed = user.profileURL ? false : true;
|
||||
const [showClaimingDialog, setShowClaimingDialog] = useState(isProfileURLClaimed);
|
||||
const [showClaimedDialog, setShowClaimedDialog] = useState(true);
|
||||
|
||||
const onFormSubmit = async ({ profileURL }: TPublicProfileFormSchema) => {
|
||||
try {
|
||||
await updatePublicProfile({
|
||||
profileURL,
|
||||
});
|
||||
setShowClaimingDialog(false);
|
||||
setShowClaimedDialog(true);
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
title: 'An error occurred',
|
||||
description: err.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'An unknown error occurred',
|
||||
variant: 'destructive',
|
||||
description:
|
||||
'We encountered an unknown error while attempting to save your details. Please try again later.',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={showClaimingDialog} onOpenChange={setShowClaimingDialog}>
|
||||
<DialogContent position="center" className="pb-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-semi-bold text-center text-2xl">
|
||||
Introducing public profile!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground/60 text-center text-sm">
|
||||
Reserve your Documenso public profile username
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Card className="relative flex flex-col items-center border-none bg-gray-50 px-6 py-6 pb-0 shadow-none">
|
||||
<code className="rounded-md border-2 border-gray-200 px-1 py-1 text-sm">
|
||||
<span>documenso.com/u/timur</span>
|
||||
</code>
|
||||
<Avatar className="dark:border-border mt-2 h-20 w-20 border-2 border-solid border-white bg-white">
|
||||
<AvatarImage className="AvatarImage" src={Timur.src} alt="Timur" />
|
||||
<AvatarFallback className="text-xs text-gray-400">Timur</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-row gap-x-2">
|
||||
Timur Ercan <Image alt="Check" src={Check} />
|
||||
</div>
|
||||
<span className="text-muted-foreground/60 text-center">
|
||||
Hey I’m Timur <br /> Pick any of the following agreements below and start signing to
|
||||
get started
|
||||
</span>
|
||||
<Card className="bg mt-2 w-full items-center shadow-none">
|
||||
<CardHeader className="p-4 text-gray-500">Documents</CardHeader>
|
||||
<hr className="mb-2" />
|
||||
<div className="mb-2 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<File className="ml-3" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-md">NDA.pdf</span>
|
||||
<span className="text-muted-foreground mt-0.5 text-xs">
|
||||
Like to discuss about my work?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mr-3" variant="default">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<div
|
||||
className="fade-overlay bg-black-100 absolute bottom-0 h-1/4 w-full"
|
||||
style={{
|
||||
background: `linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, white 75%, white 100%)`,
|
||||
}}
|
||||
></div>
|
||||
</Card>
|
||||
|
||||
<Form {...form}>
|
||||
<form className={cn('flex w-full flex-col')} onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSaving}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profileURL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Public profile URL</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
<Input type="text" className="mb-2 mt-2" {...field} />
|
||||
<div className="mt-2">
|
||||
<code className="bg-muted rounded-md px-1 py-1 text-sm">
|
||||
<span ref={textRef} id="textToCopy">
|
||||
documenso.com/u/
|
||||
</span>
|
||||
</code>
|
||||
</div>
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button type="submit" loading={isSaving}>
|
||||
Claim your username
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={false} onOpenChange={setShowClaimedDialog}>
|
||||
<DialogContent position="center" className="pb-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-semi-bold text-center text-xl">All set!</DialogTitle>
|
||||
<DialogDescription className="text-center">
|
||||
We will let you know as soon as this feature is launched
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Card className="relative px-6 py-6">
|
||||
<Card className="flex flex-col items-center px-6 py-6">
|
||||
<code className="bg-muted rounded-md px-1 py-1 text-sm">
|
||||
<span>documenso.com/u/lucas</span>
|
||||
</code>
|
||||
<Avatar className="dark:border-border mt-2 h-12 w-12 border-2 border-solid border-white">
|
||||
<AvatarImage className="AvatarImage" src={Lucas.src} alt="Lucas" />
|
||||
<AvatarFallback className="text-xs text-gray-400">Timur</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-row gap-x-2">
|
||||
Lucas Smith <Image alt="Check" src={Check} />
|
||||
</div>
|
||||
<div className="flex inline-flex h-full w-full flex-col items-center justify-center gap-3 py-2">
|
||||
<Skeleton className="w-75 h-4 animate-none rounded-full" />
|
||||
<Skeleton className="w-50 h-4 animate-none rounded-full" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="mt-2 items-center">
|
||||
<CardHeader className="p-2">Documents</CardHeader>
|
||||
<hr className="mb-2" />
|
||||
<div className="mb-2 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<File className="ml-3" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-md">NDA.pdf</span>
|
||||
<span className="text-muted-foregroun mt-0.5 text-xs">
|
||||
Like to discuss about my work?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mr-3" variant="default">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export type PublicProfileSettingsLayout = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function PublicProfileSettingsLayout({ children }: PublicProfileSettingsLayout) {
|
||||
return <div className="col-span-12 md:col-span-9">{children}</div>;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Public Profile',
|
||||
};
|
||||
|
||||
export default function PublicProfilePage() {
|
||||
return (
|
||||
<>
|
||||
<SettingsHeader
|
||||
title="Public profile"
|
||||
subtitle=""
|
||||
className="max-w-xl"
|
||||
titleChildren={
|
||||
<Link
|
||||
href="#"
|
||||
className="bg-primary dark:text-background ml-2 rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
|
||||
>
|
||||
Coming soon!
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||
|
||||
import ActivityPageBackButton from '../../../../../components/(dashboard)/settings/layout/activity-back';
|
||||
import { UserSecurityActivityDataTable } from './user-security-activity-data-table';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@ -9,11 +12,11 @@ export const metadata: Metadata = {
|
||||
export default function SettingsSecurityActivityPage() {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">Security activity</h3>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
View all recent security activity related to your account.
|
||||
</p>
|
||||
<SettingsHeader
|
||||
title="Security activity"
|
||||
subtitle="View all recent security activity related to your account."
|
||||
titleChildren={<ActivityPageBackButton />}
|
||||
/>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ export default function ErrorPage({ error }: ErrorProps) {
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="secondary"
|
||||
className="w-32"
|
||||
onClick={() => {
|
||||
void router.back();
|
||||
|
||||
@ -19,7 +19,7 @@ export default function NotFound() {
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button asChild className="w-32">
|
||||
<Button variant="secondary" asChild className="w-32">
|
||||
<Link href="/settings/teams">
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
Go Back
|
||||
|
||||
34
apps/web/src/app/(unauthenticated)/check-email/layout.tsx
Normal file
34
apps/web/src/app/(unauthenticated)/check-email/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type CheckEmailLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function CheckEmailLayout({ children }: CheckEmailLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type ForgotPasswordLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function ForgotPasswordLayout({ children }: ForgotPasswordLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-44">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center bg-contain opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -10,7 +10,7 @@ export const metadata: Metadata = {
|
||||
export default function ForgotPasswordPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">Forgot your password?</h1>
|
||||
<h1 className="text-3xl font-semibold">Forgot your password?</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
No worries, it happens! Enter your email and we'll email you a special link to reset your
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
|
||||
type UnauthenticatedLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) {
|
||||
return (
|
||||
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">{children}</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
34
apps/web/src/app/(unauthenticated)/reset-password/layout.tsx
Normal file
34
apps/web/src/app/(unauthenticated)/reset-password/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type ResetPasswordLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function ResetPasswordLayout({ children }: ResetPasswordLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-56">
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -10,7 +10,7 @@ export const metadata: Metadata = {
|
||||
export default function ResetPasswordPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">Unable to reset password</h1>
|
||||
<h1 className="text-3xl font-semibold">Unable to reset password</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
The token you have used to reset your password is either expired or it never existed. If you
|
||||
|
||||
34
apps/web/src/app/(unauthenticated)/signin/layout.tsx
Normal file
34
apps/web/src/app/(unauthenticated)/signin/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type SignInLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function SignInLayout({ children }: SignInLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-[7.2rem]">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center bg-contain opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="flex min-h-screen flex-col overflow-hidden dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -26,36 +26,31 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">Sign in to your account</h1>
|
||||
<>
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold">Sign in to your account</h1>
|
||||
|
||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||
Welcome back, we are lucky to have you.
|
||||
</p>
|
||||
|
||||
<SignInForm
|
||||
className="mt-4"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
/>
|
||||
|
||||
{process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
|
||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/signup" className="text-primary duration-200 hover:opacity-70">
|
||||
Sign up
|
||||
</Link>
|
||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||
Welcome back, we are lucky to have you.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="mt-2.5 text-center">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<hr className="my-4" />
|
||||
|
||||
<SignInForm
|
||||
className="mt-0"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
/>
|
||||
|
||||
{process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
|
||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/signup" className="text-primary duration-200 hover:opacity-70">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
21
apps/web/src/app/(unauthenticated)/signup/layout.tsx
Normal file
21
apps/web/src/app/(unauthenticated)/signup/layout.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type SignUpLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function SignUpLayout({ children }: SignUpLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main
|
||||
className="bg-sand-100 scale-90 items-center justify-center px-4 md:h-20 lg:mx-28 lg:px-8"
|
||||
style={{ height: 'calc(100vh - 80px)' }}
|
||||
>
|
||||
<div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8">{children}</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,8 +1,6 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||
|
||||
import { SignUpForm } from '~/components/forms/signup';
|
||||
@ -30,26 +28,8 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">Create a new account</h1>
|
||||
|
||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||
Create your account and start using state-of-the-art document signing. Open and beautiful
|
||||
signing is within your grasp.
|
||||
</p>
|
||||
|
||||
<SignUpForm
|
||||
className="mt-4"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
/>
|
||||
|
||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
|
||||
Sign in instead
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<>
|
||||
<SignUpForm className="mt-1" initialEmail={email || undefined} isGoogleSSOEnabled={true} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
34
apps/web/src/app/(unauthenticated)/team/layout.tsx
Normal file
34
apps/web/src/app/(unauthenticated)/team/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type TeamLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function TeamLayout({ children }: TeamLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
34
apps/web/src/app/(unauthenticated)/verify-email/layout.tsx
Normal file
34
apps/web/src/app/(unauthenticated)/verify-email/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type VerifyEmailLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function VerifyEmailLayout({ children }: VerifyEmailLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-[11.2rem]">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center bg-contain opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { File } from 'lucide-react';
|
||||
|
||||
import Check from '@documenso/assets/Check.svg';
|
||||
import Timur from '@documenso/assets/images/Timur.png';
|
||||
import backgroundPattern from '@documenso/assets/images/background-blog-og.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardFooter, CardHeader } from '@documenso/ui/primitives/card';
|
||||
|
||||
type ClaimUsernameCardProps = {
|
||||
className: string;
|
||||
};
|
||||
|
||||
export default function ClaimUsernameCard({ className }: ClaimUsernameCardProps) {
|
||||
const onSignUpClick = () => {};
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<Card className={cn('relative h-full overflow-hidden px-16 py-16 shadow-none')}>
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="absolute left-0 top-0 h-full w-full bg-cover opacity-50 dark:brightness-95 dark:contrast-[100%] dark:invert"
|
||||
/>
|
||||
<Card className="mt-28 flex flex-col items-center px-6 py-6 shadow-none">
|
||||
<code className="rounded-md border-2 border-gray-200 px-1 py-1 text-sm">
|
||||
<span>documenso.com/u/timur</span>
|
||||
</code>
|
||||
<Avatar className="dark:border-border mt-2 h-20 w-20 border-2 border-solid border-white">
|
||||
<AvatarImage className="AvatarImage" src={Timur.src} alt="Timur" />
|
||||
<AvatarFallback className="text-xs text-gray-400">Timur</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="mb-2 flex flex-row gap-x-2">
|
||||
Timur Ercan <Image alt="Check" src={Check} />
|
||||
</div>
|
||||
<span className="text-muted-foreground/60 text-center ">
|
||||
Hey I’m Timur <br /> Pick any of the following agreements below and <br /> start signing
|
||||
to get started
|
||||
</span>
|
||||
<Card className="mt-2 w-full items-center shadow-none">
|
||||
<CardHeader className="p-4 text-gray-500">Documents</CardHeader>
|
||||
<hr className="mb-2" />
|
||||
<div className="mb-2 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<File className="text-muted-foreground ml-3" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-md">NDA.pdf</span>
|
||||
<span className="text-muted-foreground mt-0.5 text-xs">
|
||||
Like to discuss about my work?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mr-3 px-6" variant="default">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
<hr className="mb-2" />
|
||||
<div className="mb-2 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<File className="text-muted-foreground ml-3" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-md">NDA.pdf</span>
|
||||
<span className="text-muted-foreground mt-0.5 text-xs">
|
||||
Like to discuss about my work?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mr-3 px-6" variant="default">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<CardFooter className="mt-32 justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Claim Community Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
-80%
|
||||
</span>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import LogoImage from '@documenso/assets/logo.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import { NewHamburgerMenu } from './new-mobile-hamburger';
|
||||
import { NewMobileNavigation } from './new-mobile-navigation';
|
||||
|
||||
export type HeaderProps = HTMLAttributes<HTMLElement>;
|
||||
|
||||
export const NewHeader = ({ className, ...props }: HeaderProps) => {
|
||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className={cn('flex items-center justify-between', className)} {...props}>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
|
||||
<Image
|
||||
src={LogoImage}
|
||||
alt="Documenso Logo"
|
||||
className="dark:invert"
|
||||
width={170}
|
||||
height={25}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center gap-x-6 md:flex">
|
||||
<Link
|
||||
href="https://documenso.com/pricing"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Pricing
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://documenso.com/blog"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://documenso.com/open"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Open Startup
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/signin"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="/signup"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
<span className="bg-primary dark:text-background rounded-full px-3 py-2 text-xs">
|
||||
Sign up
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<NewHamburgerMenu
|
||||
onToggleMenuOpen={() => setIsHamburgerMenuOpen((v) => !v)}
|
||||
isMenuOpen={isHamburgerMenuOpen}
|
||||
/>
|
||||
<NewMobileNavigation
|
||||
isMenuOpen={isHamburgerMenuOpen}
|
||||
onMenuOpenChange={setIsHamburgerMenuOpen}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { Menu, X } from 'lucide-react';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export interface HamburgerMenuProps {
|
||||
isMenuOpen: boolean;
|
||||
onToggleMenuOpen?: () => void;
|
||||
}
|
||||
|
||||
export const NewHamburgerMenu = ({ isMenuOpen, onToggleMenuOpen }: HamburgerMenuProps) => {
|
||||
return (
|
||||
<div className="flex md:hidden">
|
||||
<Button variant="outline" className="z-20 w-10 p-0" onClick={onToggleMenuOpen}>
|
||||
{isMenuOpen ? <X /> : <Menu />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,151 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
import { LiaDiscord } from 'react-icons/lia';
|
||||
import { LuGithub } from 'react-icons/lu';
|
||||
|
||||
import LogoImage from '@documenso/assets/logo.png';
|
||||
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
||||
|
||||
export type MobileNavigationProps = {
|
||||
isMenuOpen: boolean;
|
||||
onMenuOpenChange?: (_value: boolean) => void;
|
||||
};
|
||||
|
||||
export const MENU_NAVIGATION_LINKS = [
|
||||
{
|
||||
href: 'https://documenso.com/singleplayer',
|
||||
text: 'Singleplayer',
|
||||
},
|
||||
{
|
||||
href: 'https://documenso.com/blog',
|
||||
text: 'Blog',
|
||||
},
|
||||
{
|
||||
href: 'https://documenso.com/pricing',
|
||||
text: 'Pricing',
|
||||
},
|
||||
{
|
||||
href: 'https://documenso.com/open',
|
||||
text: 'Open Startup',
|
||||
},
|
||||
{
|
||||
href: 'https://status.documenso.com',
|
||||
text: 'Status',
|
||||
},
|
||||
{
|
||||
href: 'mailto:support@documenso.com',
|
||||
text: 'Support',
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
href: 'https://documenso.com/privacy',
|
||||
text: 'Privacy',
|
||||
},
|
||||
{
|
||||
href: '/signin',
|
||||
text: 'Sign in',
|
||||
},
|
||||
{
|
||||
href: '/signup',
|
||||
text: 'Sign up',
|
||||
},
|
||||
];
|
||||
|
||||
export const NewMobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const handleMenuItemClick = () => {
|
||||
onMenuOpenChange?.(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
||||
<SheetContent className="w-full max-w-[400px]">
|
||||
<Link href="/" className="z-10" onClick={handleMenuItemClick}>
|
||||
<Image
|
||||
src={LogoImage}
|
||||
alt="Documenso Logo"
|
||||
className="dark:invert"
|
||||
width={170}
|
||||
height={25}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<motion.div
|
||||
className="mt-12 flex w-full flex-col items-start gap-y-4"
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
transition={{
|
||||
staggerChildren: 0.03,
|
||||
}}
|
||||
>
|
||||
{MENU_NAVIGATION_LINKS.map(({ href, text, target }) => (
|
||||
<motion.div
|
||||
key={href}
|
||||
variants={{
|
||||
initial: {
|
||||
opacity: 0,
|
||||
x: shouldReduceMotion ? 0 : 100,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: 'backInOut',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
||||
href={href}
|
||||
onClick={() => handleMenuItemClick()}
|
||||
target={target}
|
||||
>
|
||||
{href === 'https://app.documenso.com/signup' ? (
|
||||
<span className="bg-primary dark:text-background rounded-full px-3 py-2 text-xl">
|
||||
{text}
|
||||
</span>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<div className="mx-auto mt-8 flex w-full flex-wrap items-center gap-x-4 gap-y-4 ">
|
||||
<Link
|
||||
href="https://twitter.com/documenso"
|
||||
target="_blank"
|
||||
className="text-foreground hover:text-foreground/80"
|
||||
>
|
||||
<FaXTwitter className="h-6 w-6" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://github.com/documenso/documenso"
|
||||
target="_blank"
|
||||
className="text-foreground hover:text-foreground/80"
|
||||
>
|
||||
<LuGithub className="h-6 w-6" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
className="text-foreground hover:text-foreground/80"
|
||||
>
|
||||
<LiaDiscord className="h-7 w-7" />
|
||||
</Link>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export default function ActivityPageBackButton() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className="flex-shrink-0"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
void router.back();
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { CreditCard, Lock, User, Users } from 'lucide-react';
|
||||
import { CreditCard, Globe2, Lock, User, Users } from 'lucide-react';
|
||||
|
||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -78,6 +78,25 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link href="#">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/public-profile') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Globe2 className="mr-2 h-5 w-5" />
|
||||
Public profile
|
||||
<Link
|
||||
href="#"
|
||||
className="bg-primary dark:text-background ml-2 rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
|
||||
>
|
||||
Coming soon!
|
||||
</Link>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,21 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
export type SettingsHeaderProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
children?: React.ReactNode;
|
||||
titleChildren?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const SettingsHeader = ({ children, title, subtitle }: SettingsHeaderProps) => {
|
||||
export const SettingsHeader = ({
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
titleChildren,
|
||||
className,
|
||||
}: SettingsHeaderProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className={cn('flex flex-row items-center justify-between', className)}>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">{title}</h3>
|
||||
|
||||
<p className="text-muted-foreground text-sm md:mt-2">{subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div>{titleChildren}</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { CreditCard, Lock, User, Users } from 'lucide-react';
|
||||
import { CreditCard, Globe2, Lock, User, Users } from 'lucide-react';
|
||||
|
||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -81,6 +81,25 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link href="#">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/public-profile') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Globe2 className="mr-2 h-5 w-5" />
|
||||
Public profile
|
||||
<Link
|
||||
href="#"
|
||||
className="bg-primary dark:text-background ml-2 rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
|
||||
>
|
||||
Coming soon!
|
||||
</Link>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -167,135 +169,147 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={cn('flex w-full flex-col gap-y-4', className)}
|
||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso dark:hover:opacity-90"
|
||||
<div className={cn('mt-1')}>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={cn('flex w-full flex-col gap-y-4', className)}
|
||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||
>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
{isGoogleSSOEnabled && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">Or continue with</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithGoogleClick}
|
||||
>
|
||||
<FcGoogle className="mr-2 h-5 w-5" />
|
||||
Google
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<Dialog
|
||||
open={isTwoFactorAuthenticationDialogOpen}
|
||||
onOpenChange={onCloseTwoFactorAuthenticationDialog}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Two-Factor Authentication</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={isSubmitting}>
|
||||
{twoFactorAuthenticationMethod === 'totp' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="totpCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Authentication Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="john@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{twoFactorAuthenticationMethod === 'backup' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="backupCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel> Backup Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
<PasswordInput placeholder="Password" {...field} />
|
||||
<p className="mt-2.5 text-right">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-muted-foreground/60 text-sm duration-200 hover:opacity-70"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onToggleTwoFactorAuthenticationMethodClick}
|
||||
>
|
||||
{twoFactorAuthenticationMethod === 'totp'
|
||||
? 'Use Backup Code'
|
||||
: 'Use Authenticator'}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso dark:hover:opacity-90"
|
||||
>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Form>
|
||||
{isGoogleSSOEnabled && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">Or continue with</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithGoogleClick}
|
||||
>
|
||||
<FcGoogle className="mr-2 h-5 w-5" />
|
||||
Google
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<Dialog
|
||||
open={isTwoFactorAuthenticationDialogOpen}
|
||||
onOpenChange={onCloseTwoFactorAuthenticationDialog}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Two-Factor Authentication</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={isSubmitting}>
|
||||
{twoFactorAuthenticationMethod === 'totp' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="totpCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Authentication Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{twoFactorAuthenticationMethod === 'backup' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="backupCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel> Backup Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onToggleTwoFactorAuthenticationMethodClick}
|
||||
>
|
||||
{twoFactorAuthenticationMethod === 'totp'
|
||||
? 'Use Backup Code'
|
||||
: 'Use Authenticator'}
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -12,6 +17,7 @@ import { trpc } from '@documenso/trpc/react';
|
||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@ -25,6 +31,16 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import ClaimUsernameCard from '../(dashboard)/claim-username-card/claim-username-card';
|
||||
|
||||
export const STEP = {
|
||||
SIGNUP: 'SIGNUP',
|
||||
CLAIM: 'CLAIM',
|
||||
} as const;
|
||||
|
||||
type StepKeys = keyof typeof STEP;
|
||||
type StepValues = (typeof STEP)[StepKeys];
|
||||
|
||||
const SIGN_UP_REDIRECT_PATH = '/documents';
|
||||
|
||||
export const ZSignUpFormSchema = z
|
||||
@ -33,6 +49,7 @@ export const ZSignUpFormSchema = z
|
||||
email: z.string().email().min(1),
|
||||
password: ZPasswordSchema,
|
||||
signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
|
||||
profileURL: z.string().trim().min(1, { message: 'Please enter a valid URL slug.' }),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@ -55,6 +72,13 @@ export type SignUpFormProps = {
|
||||
export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: SignUpFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const analytics = useAnalytics();
|
||||
const searchParams = useSearchParams();
|
||||
const [step, setStep] = useState<StepValues>(STEP.SIGNUP);
|
||||
|
||||
let src: string | null = null;
|
||||
if (searchParams) {
|
||||
src = searchParams.get('src');
|
||||
}
|
||||
|
||||
const form = useForm<TSignUpFormSchema>({
|
||||
values: {
|
||||
@ -62,18 +86,33 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
email: initialEmail ?? '',
|
||||
password: '',
|
||||
signature: '',
|
||||
profileURL: '',
|
||||
},
|
||||
resolver: zodResolver(ZSignUpFormSchema),
|
||||
});
|
||||
|
||||
const isSubmitting = form.formState.isSubmitting;
|
||||
const isValid = form.formState.isValid;
|
||||
|
||||
const signature = form.watch('signature');
|
||||
|
||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
||||
const { mutateAsync: updatePublicProfile } = trpc.profile.updatePublicProfile.useMutation();
|
||||
|
||||
const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
|
||||
const onFormSubmit = async ({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
signature,
|
||||
profileURL,
|
||||
}: TSignUpFormSchema) => {
|
||||
try {
|
||||
await signup({ name, email, password, signature });
|
||||
|
||||
await updatePublicProfile({
|
||||
profileURL,
|
||||
});
|
||||
|
||||
await signIn('credentials', {
|
||||
email,
|
||||
password,
|
||||
@ -83,6 +122,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
analytics.capture('App: User Sign Up', {
|
||||
email,
|
||||
timestamp: new Date().toISOString(),
|
||||
custom_campaign_params: { src },
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
@ -102,6 +142,16 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
}
|
||||
};
|
||||
|
||||
const onNextStepClick = () => {
|
||||
if (step === STEP.SIGNUP) {
|
||||
setStep(STEP.CLAIM);
|
||||
|
||||
setTimeout(() => {
|
||||
document.querySelector<HTMLElement>('#signature')?.focus();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const onSignUpWithGoogleClick = async () => {
|
||||
try {
|
||||
await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
||||
@ -115,107 +165,199 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
}
|
||||
};
|
||||
|
||||
const stepsRemaining = useMemo(() => {
|
||||
if (step === STEP.CLAIM) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}, [step]);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={cn('flex w-full flex-col gap-y-4', className)}
|
||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<>
|
||||
<ClaimUsernameCard className="col-span-12 gap-y-4 lg:col-span-7" />
|
||||
<Card className="col-span-12 gap-y-4 bg-gray-50 px-6 py-6 shadow-none lg:col-span-5">
|
||||
<div className="w-full">
|
||||
{step === STEP.SIGNUP && (
|
||||
<>
|
||||
<h1 className="text-3xl font-semibold">Create a new account</h1>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||
Create your account and start using state-of-the-art document signing. Open and
|
||||
beautiful signing is within your grasp.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.CLAIM && (
|
||||
<>
|
||||
<h1 className="text-3xl font-semibold">Claim your username now</h1>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="signature"
|
||||
render={({ field: { onChange } }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Sign Here</FormLabel>
|
||||
<FormControl>
|
||||
<SignaturePad
|
||||
className="h-36 w-full"
|
||||
disabled={isSubmitting}
|
||||
containerClassName="mt-2 rounded-lg border bg-background"
|
||||
onChange={(v) => onChange(v ?? '')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso dark:hover:opacity-90"
|
||||
>
|
||||
{isSubmitting ? 'Signing up...' : 'Sign Up'}
|
||||
</Button>
|
||||
|
||||
{isGoogleSSOEnabled && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">Or</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant={'outline'}
|
||||
className="bg-background text-muted-foreground border"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignUpWithGoogleClick}
|
||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||
You will get notified & be able to set up your documenso public profile when we
|
||||
launch the feature
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<hr className="mb-6 mt-4" />
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={cn('flex h-full w-full flex-col gap-y-4', className)}
|
||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<FcGoogle className="mr-2 h-5 w-5" />
|
||||
Sign Up with Google
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}>
|
||||
<div className={cn(step === STEP.SIGNUP && 'hidden')}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profileURL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Public profile URL</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
<Input id="username" type="text" className="mb-2 mt-2" {...field} />
|
||||
<div className="mt-2">
|
||||
<code className="bg-muted rounded-md px-1 py-1 text-sm">
|
||||
documenso.com/u/
|
||||
</code>
|
||||
</div>
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('space-y-2', step === STEP.CLAIM && 'invisible')}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Full Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" className="bg-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" className="bg-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput className="bg-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="signature"
|
||||
render={({ field: { onChange } }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Sign Here</FormLabel>
|
||||
<FormControl>
|
||||
<SignaturePad
|
||||
id="signatureText"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
containerClassName="mt-2 rounded-lg border bg-background"
|
||||
onChange={(v) => onChange(v ?? '')}
|
||||
height={200}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{isGoogleSSOEnabled && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">Or</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant={'outline'}
|
||||
className="bg-background text-muted-foreground border"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignUpWithGoogleClick}
|
||||
>
|
||||
<FcGoogle className="mr-2 h-5 w-5" />
|
||||
Sign Up with Google
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<p className="text-muted-foreground text-left text-sm">
|
||||
Already have an account?{' '}
|
||||
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
|
||||
Sign in instead
|
||||
</Link>
|
||||
</p>
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{isValid ? 'Claim username' : `Basic details ${stepsRemaining}/2`}
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground block text-xs md:hidden">Minimise contract</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-background relative h-[2px] w-full">
|
||||
<div
|
||||
className={cn('bg-primary/60 absolute inset-y-0 left-0 duration-200', {
|
||||
'w-1/2': stepsRemaining === 1,
|
||||
'w-full': isValid,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{!isValid && (
|
||||
<Button
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso ml-auto w-52 dark:hover:opacity-90"
|
||||
onClick={() => onNextStepClick()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
{isValid && (
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso ml-auto w-52 dark:hover:opacity-90"
|
||||
>
|
||||
Complete
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -46,7 +46,7 @@ export default function NotFoundPartial({ children }: NotFoundPartialProps) {
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="secondary"
|
||||
className="w-32"
|
||||
onClick={() => {
|
||||
void router.back();
|
||||
|
||||
@ -32,6 +32,7 @@ export function PostHogPageview() {
|
||||
// Do nothing.
|
||||
});
|
||||
},
|
||||
custom_campaign_params: ['src'],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
3
packages/assets/Check.svg
Normal file
3
packages/assets/Check.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.54572 1.10579C9.57786 -0.0821356 11.4223 -0.0821393 12.4545 1.10579L13.2585 2.03112C13.3917 2.18452 13.5947 2.25838 13.7954 2.22654L15.0061 2.03449C16.5603 1.78794 17.9732 2.97353 18.0003 4.54698L18.0214 5.77262C18.0249 5.97581 18.1329 6.16284 18.3071 6.26746L19.358 6.89855C20.7071 7.70873 21.0274 9.52517 20.0368 10.7479L19.2651 11.7004C19.1372 11.8583 19.0997 12.0709 19.1659 12.2631L19.5652 13.422C20.0779 14.9098 19.1557 16.5072 17.6109 16.8071L16.4075 17.0407C16.208 17.0794 16.0426 17.2182 15.9698 17.408L15.5308 18.5525C14.9672 20.0218 13.234 20.6526 11.8578 19.8893L10.7858 19.2948C10.6081 19.1962 10.3921 19.1962 10.2144 19.2948L9.14242 19.8893C7.76623 20.6526 6.033 20.0218 5.4694 18.5525L5.03038 17.408C4.9576 17.2182 4.79216 17.0794 4.59268 17.0407L3.38932 16.8071C1.84448 16.5072 0.922245 14.9098 1.43495 13.422L1.83431 12.2631C1.90052 12.0709 1.86302 11.8583 1.7351 11.7004L0.963432 10.7479C-0.0272148 9.52517 0.293068 7.70873 1.64218 6.89855L2.69306 6.26746C2.86728 6.16284 2.97526 5.97581 2.97875 5.77263L2.99985 4.54699C3.02694 2.97354 4.43987 1.78794 5.99413 2.03449L7.20481 2.22654C7.40551 2.25838 7.60845 2.18452 7.74173 2.03112L8.54572 1.10579ZM13.7072 9.42195C14.0977 9.03143 14.0977 8.39826 13.7072 8.00774C13.3167 7.61721 12.6835 7.61721 12.293 8.00774L9.5001 10.8006L8.7072 10.0077C8.31667 9.61721 7.68351 9.61721 7.29298 10.0077C6.90246 10.3983 6.90246 11.0314 7.29298 11.422L8.43944 12.5684C9.02522 13.1542 9.97497 13.1542 10.5608 12.5684L13.7072 9.42195Z" fill="#7AC455"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
packages/assets/images/Lucas.png
Normal file
BIN
packages/assets/images/Lucas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
packages/assets/images/Timur.png
Normal file
BIN
packages/assets/images/Timur.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
BIN
packages/assets/images/background-lw-2.png
Normal file
BIN
packages/assets/images/background-lw-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
35
packages/lib/server-only/user/update-public-profile.ts
Normal file
35
packages/lib/server-only/user/update-public-profile.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { User, UserProfile } from '@documenso/prisma/client';
|
||||
|
||||
import { getUserById } from './get-user-by-id';
|
||||
|
||||
export type UpdatePublicProfileOptions = {
|
||||
id: User['id'];
|
||||
profileURL: UserProfile['profileURL'];
|
||||
};
|
||||
|
||||
export const updatePublicProfile = async ({ id, profileURL }: UpdatePublicProfileOptions) => {
|
||||
const user = await getUserById({ id });
|
||||
// Existence check
|
||||
await prisma.userProfile.findFirstOrThrow({
|
||||
where: {
|
||||
profileURL: user.profileURL ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await tx.userProfile.create({
|
||||
data: {
|
||||
profileURL,
|
||||
},
|
||||
});
|
||||
await tx.userProfile.update({
|
||||
where: {
|
||||
profileURL: user.profileURL ?? undefined,
|
||||
},
|
||||
data: {
|
||||
profileURL: profileURL,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[profileURL]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "profileURL" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserProfile" (
|
||||
"profileURL" TEXT NOT NULL,
|
||||
"profileBio" TEXT,
|
||||
|
||||
CONSTRAINT "UserProfile_pkey" PRIMARY KEY ("profileURL")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserProfile_profileURL_key" ON "UserProfile"("profileURL");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_profileURL_key" ON "User"("profileURL");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_profileURL_fkey" FOREIGN KEY ("profileURL") REFERENCES "UserProfile"("profileURL") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -43,6 +43,9 @@ model User {
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorBackupCodes String?
|
||||
profileURL String? @unique
|
||||
|
||||
UserProfile UserProfile? @relation(fields: [profileURL], references: [profileURL], onDelete: Cascade)
|
||||
|
||||
VerificationToken VerificationToken[]
|
||||
Template Template[]
|
||||
@ -51,6 +54,13 @@ model User {
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
model UserProfile {
|
||||
profileURL String @id @unique
|
||||
profileBio String?
|
||||
|
||||
User User?
|
||||
}
|
||||
|
||||
enum UserSecurityAuditLogType {
|
||||
ACCOUNT_PROFILE_UPDATE
|
||||
ACCOUNT_SSO_LINK
|
||||
|
||||
@ -7,6 +7,7 @@ import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
|
||||
import { sendConfirmationToken } from '@documenso/lib/server-only/user/send-confirmation-token';
|
||||
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
|
||||
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
|
||||
import { updatePublicProfile } from '@documenso/lib/server-only/user/update-public-profile';
|
||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
|
||||
import { adminProcedure, authenticatedProcedure, procedure, router } from '../trpc';
|
||||
@ -18,6 +19,7 @@ import {
|
||||
ZRetrieveUserByIdQuerySchema,
|
||||
ZUpdatePasswordMutationSchema,
|
||||
ZUpdateProfileMutationSchema,
|
||||
ZUpdatePublicProfileMutationSchema,
|
||||
} from './schema';
|
||||
|
||||
export const profileRouter = router({
|
||||
@ -73,6 +75,27 @@ export const profileRouter = router({
|
||||
}
|
||||
}),
|
||||
|
||||
updatePublicProfile: authenticatedProcedure
|
||||
.input(ZUpdatePublicProfileMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { profileURL } = input;
|
||||
|
||||
return await updatePublicProfile({
|
||||
id: ctx.user.id,
|
||||
profileURL,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message:
|
||||
'We were unable to update your public profile. Please review the information you provided and try again.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
updatePassword: authenticatedProcedure
|
||||
.input(ZUpdatePasswordMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@ -16,6 +16,10 @@ export const ZUpdateProfileMutationSchema = z.object({
|
||||
signature: z.string(),
|
||||
});
|
||||
|
||||
export const ZUpdatePublicProfileMutationSchema = z.object({
|
||||
profileURL: z.string().min(1),
|
||||
});
|
||||
|
||||
export const ZUpdatePasswordMutationSchema = z.object({
|
||||
currentPassword: ZCurrentPasswordSchema,
|
||||
password: ZPasswordSchema,
|
||||
|
||||
32
packages/ui/primitives/announcement-bar.tsx
Normal file
32
packages/ui/primitives/announcement-bar.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
interface AnnouncementBarProps {
|
||||
isShown: boolean;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const AnnouncementBar: React.FC<AnnouncementBarProps> = ({ isShown, className }) => {
|
||||
return (
|
||||
isShown && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full w-full items-center justify-center gap-4 border-b-2 p-1',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="text-center">
|
||||
<span className="text-sm text-white">Claim your documenso public profile URL now!</span>{' '}
|
||||
<span className="text-sm font-medium text-white">documenso.com/u/yourname</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-4 rounded-lg bg-white px-3 py-1">
|
||||
<div className="text-xs text-gray-900">
|
||||
<Link href="https://app.documenso.com">Claim now</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
@ -17,6 +17,7 @@ export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChang
|
||||
onChange?: (_signatureDataUrl: string | null) => void;
|
||||
containerClassName?: string;
|
||||
disabled?: boolean;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export const SignaturePad = ({
|
||||
@ -25,6 +26,7 @@ export const SignaturePad = ({
|
||||
defaultValue,
|
||||
onChange,
|
||||
disabled = false,
|
||||
height,
|
||||
...props
|
||||
}: SignaturePadProps) => {
|
||||
const $el = useRef<HTMLCanvasElement>(null);
|
||||
@ -230,6 +232,7 @@ export const SignaturePad = ({
|
||||
onPointerUp={(event) => onMouseUp(event)}
|
||||
onPointerLeave={(event) => onMouseLeave(event)}
|
||||
onPointerEnter={(event) => onMouseEnter(event)}
|
||||
height={height}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import * as TogglePrimitive from '@radix-ui/react-toggle';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user