mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 08:42:12 +10:00
feat: the rest of the owl
This commit is contained in:
@ -29,7 +29,7 @@ export const ClaimProfileAlertDialog = ({ className, user }: ClaimProfileAlertDi
|
||||
<div>
|
||||
<AlertTitle>Claim your profile</AlertTitle>
|
||||
<AlertDescription className="mr-2">
|
||||
Profiles are coming soon! Claim your profile URL now to reserve your corner of the
|
||||
Profiles are coming soon! Claim your profile username now to reserve your corner of the
|
||||
signing revolution.
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
@ -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="secondary"
|
||||
variant="ghost"
|
||||
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 variant="secondary" asChild className="w-32">
|
||||
<Button asChild className="w-32">
|
||||
<Link href="/settings/teams">
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
Go Back
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
import communityCardsImage from '@documenso/assets/images/community-cards.png';
|
||||
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';
|
||||
import { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton';
|
||||
import { SignUpFormV2 } from '~/components/forms/v2/signup';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Sign Up',
|
||||
@ -37,57 +33,10 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-screen max-w-screen-2xl justify-center gap-x-12 px-4 md:px-16">
|
||||
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
||||
<div className="absolute -inset-8 -z-[2] backdrop-blur">
|
||||
<Image
|
||||
src={communityCardsImage}
|
||||
fill={true}
|
||||
alt="community-cards"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-background/50 absolute -inset-8 -z-[1] backdrop-blur-[2px]" />
|
||||
|
||||
<div className="relative flex h-full w-full flex-col items-center justify-evenly">
|
||||
<div className="bg-background rounded-2xl border px-4 py-1 text-sm font-medium">
|
||||
User profiles are coming soon!
|
||||
</div>
|
||||
|
||||
<UserProfileSkeleton
|
||||
user={{ name: 'Timur Ercan', email: 'timur@documenso.com', url: 'timur' }}
|
||||
rows={2}
|
||||
className="bg-background border-border w-full max-w-md rounded-2xl border shadow-md"
|
||||
/>
|
||||
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-border dark:bg-background z-10 max-w-lg rounded-xl border bg-neutral-100 p-6">
|
||||
<h1 className="text-2xl font-semibold">Create a new account</h1>
|
||||
|
||||
<p className="text-muted-foreground 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>
|
||||
|
||||
<hr className="-mx-6 my-4" />
|
||||
|
||||
<SignUpForm
|
||||
className="mt-1"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED || true}
|
||||
/>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<SignUpFormV2
|
||||
className="w-screen max-w-screen-2xl px-4 md:px-16"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
80
apps/web/src/app/(unauthenticated)/signup/view.tsx
Normal file
80
apps/web/src/app/(unauthenticated)/signup/view.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import communityCardsImage from '@documenso/assets/images/community-cards.png';
|
||||
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||
|
||||
import { SignUpFormV2 } from '~/components/forms/v2/signup';
|
||||
import { UserProfileTimur } from '~/components/ui/user-profile-timur';
|
||||
|
||||
type SignUpPageViewProps = {
|
||||
email?: string;
|
||||
isGoogleSSOEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const SignUpPageView = ({ email, isGoogleSSOEnabled }: SignUpPageViewProps) => {
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
return (
|
||||
<div className="flex w-screen max-w-screen-2xl justify-center gap-x-12 px-4 md:px-16">
|
||||
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
||||
<div className="absolute -inset-8 -z-[2] backdrop-blur">
|
||||
<Image
|
||||
src={communityCardsImage}
|
||||
fill={true}
|
||||
alt="community-cards"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-background/50 absolute -inset-8 -z-[1] backdrop-blur-[2px]" />
|
||||
|
||||
<div className="relative flex h-full w-full flex-col items-center justify-evenly">
|
||||
<div className="bg-background rounded-2xl border px-4 py-1 text-sm font-medium">
|
||||
User profiles are coming soon!
|
||||
</div>
|
||||
|
||||
{
|
||||
<UserProfileTimur
|
||||
rows={2}
|
||||
className="bg-background border-border w-full max-w-md rounded-2xl border shadow-md"
|
||||
/>
|
||||
}
|
||||
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-border dark:bg-background z-10 min-h-[min(800px,80vh)] w-full max-w-lg rounded-xl border bg-neutral-100 p-6">
|
||||
<Stepper currentStep={step} onStepChanged={setStep} setCurrentStep={setStep}>
|
||||
<>
|
||||
<h1 className="text-2xl font-semibold">Create a new account</h1>
|
||||
|
||||
<p className="text-muted-foreground 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>
|
||||
|
||||
<hr className="-mx-6 my-4" />
|
||||
|
||||
<SignUpFormV2
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={isGoogleSSOEnabled || true}
|
||||
/>
|
||||
|
||||
<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>
|
||||
</>
|
||||
</Stepper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,93 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { BadgeCheck, File } from 'lucide-react';
|
||||
|
||||
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';
|
||||
|
||||
export default function ClaimUsernameCard() {
|
||||
const onSignUpClick = () => {};
|
||||
return (
|
||||
<div className="relative">
|
||||
<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-[100%] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
<Card className={cn('relative px-16 py-16')}>
|
||||
<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/timur</span>
|
||||
</code>
|
||||
<Avatar className="dark:border-border mt-2 h-12 w-12 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="flex flex-row gap-x-2">
|
||||
Timur Ercan <BadgeCheck fill="#A2E771" />
|
||||
</div>
|
||||
<span className="text-center">
|
||||
Hey I’m Timur <br /> Pick any of the following agreements below and start signing to get
|
||||
started
|
||||
</span>
|
||||
</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>
|
||||
<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>
|
||||
<CardFooter className="mt-20 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>
|
||||
);
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
@ -1,20 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
@ -1,151 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
@ -9,6 +9,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@ -35,7 +36,14 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { UserProfileSkeleton } from '../ui/user-profile-skeleton';
|
||||
|
||||
export const ZClaimPublicProfileFormSchema = z.object({
|
||||
url: z.string().trim().min(1, { message: 'Please enter a valid URL slug.' }),
|
||||
url: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.min(1, { message: 'Please enter a valid username.' })
|
||||
.regex(/^[a-z0-9-]+$/, {
|
||||
message: 'Username can only container alphanumeric characters and dashes.',
|
||||
}),
|
||||
});
|
||||
|
||||
export type TClaimPublicProfileFormSchema = z.infer<typeof ZClaimPublicProfileFormSchema>;
|
||||
@ -57,6 +65,8 @@ export const ClaimPublicProfileDialogForm = ({
|
||||
|
||||
const [claimed, setClaimed] = useState(false);
|
||||
|
||||
const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
|
||||
|
||||
const form = useForm<TClaimPublicProfileFormSchema>({
|
||||
values: {
|
||||
url: user.url || '',
|
||||
@ -82,12 +92,17 @@ export const ClaimPublicProfileDialogForm = ({
|
||||
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
|
||||
form.setError('url', {
|
||||
type: 'manual',
|
||||
message: 'This URL is already taken',
|
||||
message: 'This username is already taken',
|
||||
});
|
||||
} else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
|
||||
form.setError('url', {
|
||||
type: 'manual',
|
||||
message: error.message,
|
||||
});
|
||||
} else if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
|
||||
toast({
|
||||
title: 'An error occurred',
|
||||
description: err.message,
|
||||
description: error.userMessage ?? error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
@ -131,7 +146,7 @@ export const ClaimPublicProfileDialogForm = ({
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Public profile URL</FormLabel>
|
||||
<FormLabel>Public profile username</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input type="text" className="mb-2 mt-2" {...field} />
|
||||
@ -140,7 +155,7 @@ export const ClaimPublicProfileDialogForm = ({
|
||||
<FormMessage />
|
||||
|
||||
<div className="bg-muted/50 text-muted-foreground mt-2 inline-block truncate rounded-md px-2 py-1 text-sm">
|
||||
documenso.com/u/{field.value || '<username>'}
|
||||
{baseUrl.host}/u/{field.value || '<username>'}
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@ -61,7 +61,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const src = searchParams?.get('src') ?? null;
|
||||
const utmSrc = searchParams?.get('utm_source') ?? null;
|
||||
|
||||
const form = useForm<TSignUpFormSchema>({
|
||||
values: {
|
||||
@ -93,7 +93,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
analytics.capture('App: User Sign Up', {
|
||||
email,
|
||||
timestamp: new Date().toISOString(),
|
||||
custom_campaign_params: { src },
|
||||
custom_campaign_params: { src: utmSrc },
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
|
||||
463
apps/web/src/components/forms/v2/signup.tsx
Normal file
463
apps/web/src/components/forms/v2/signup.tsx
Normal file
@ -0,0 +1,463 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { FcGoogle } from 'react-icons/fc';
|
||||
import { z } from 'zod';
|
||||
|
||||
import communityCardsImage from '@documenso/assets/images/community-cards.png';
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { TRPCClientError } from '@documenso/trpc/client';
|
||||
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 {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
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 { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton';
|
||||
import { UserProfileTimur } from '~/components/ui/user-profile-timur';
|
||||
|
||||
const SIGN_UP_REDIRECT_PATH = '/documents';
|
||||
|
||||
type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
|
||||
|
||||
export const ZSignUpFormV2Schema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
|
||||
email: z.string().email().min(1),
|
||||
password: ZPasswordSchema,
|
||||
signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
|
||||
url: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.min(1, { message: 'We need a username to create your profile' })
|
||||
.regex(/^[a-z0-9-]+$/, {
|
||||
message: 'Username can only container alphanumeric characters and dashes.',
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const { name, email, password } = data;
|
||||
return !password.includes(name) && !password.includes(email.split('@')[0]);
|
||||
},
|
||||
{
|
||||
message: 'Password should not be common or based on personal information',
|
||||
},
|
||||
);
|
||||
|
||||
export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>;
|
||||
|
||||
export type SignUpFormV2Props = {
|
||||
className?: string;
|
||||
initialEmail?: string;
|
||||
isGoogleSSOEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const SignUpFormV2 = ({
|
||||
className,
|
||||
initialEmail,
|
||||
isGoogleSSOEnabled,
|
||||
}: SignUpFormV2Props) => {
|
||||
const { toast } = useToast();
|
||||
const analytics = useAnalytics();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [step, setStep] = useState<SignUpStep>('BASIC_DETAILS');
|
||||
|
||||
const utmSrc = searchParams?.get('utm_source') ?? null;
|
||||
|
||||
const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
|
||||
|
||||
const form = useForm<TSignUpFormV2Schema>({
|
||||
values: {
|
||||
name: '',
|
||||
email: initialEmail ?? '',
|
||||
password: '',
|
||||
signature: '',
|
||||
url: '',
|
||||
},
|
||||
mode: 'onBlur',
|
||||
resolver: zodResolver(ZSignUpFormV2Schema),
|
||||
});
|
||||
|
||||
const isSubmitting = form.formState.isSubmitting;
|
||||
|
||||
const name = form.watch('name');
|
||||
const url = form.watch('url');
|
||||
|
||||
// To continue we need to make sure name, email, password and signature are valid
|
||||
const canContinue =
|
||||
form.formState.dirtyFields.name &&
|
||||
form.formState.errors.name === undefined &&
|
||||
form.formState.dirtyFields.email &&
|
||||
form.formState.errors.email === undefined &&
|
||||
form.formState.dirtyFields.password &&
|
||||
form.formState.errors.password === undefined &&
|
||||
form.formState.dirtyFields.signature &&
|
||||
form.formState.errors.signature === undefined;
|
||||
|
||||
console.log({ formSTate: form.formState });
|
||||
|
||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
||||
|
||||
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormV2Schema) => {
|
||||
try {
|
||||
await signup({ name, email, password, signature, url });
|
||||
|
||||
router.push(`/unverified-account`);
|
||||
|
||||
toast({
|
||||
title: 'Registration Successful',
|
||||
description:
|
||||
'You have successfully registered. Please verify your account by clicking on the link you received in the email.',
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
analytics.capture('App: User Sign Up', {
|
||||
email,
|
||||
timestamp: new Date().toISOString(),
|
||||
custom_campaign_params: { src: utmSrc },
|
||||
});
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
|
||||
form.setError('url', {
|
||||
type: 'manual',
|
||||
message: 'This username has already been taken',
|
||||
});
|
||||
} else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
|
||||
form.setError('url', {
|
||||
type: 'manual',
|
||||
message: error.message,
|
||||
});
|
||||
} else 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',
|
||||
description:
|
||||
'We encountered an unknown error while attempting to sign you up. Please try again later.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSignUpWithGoogleClick = async () => {
|
||||
try {
|
||||
await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'An unknown error occurred',
|
||||
description:
|
||||
'We encountered an unknown error while attempting to sign you Up. Please try again later.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex justify-center gap-x-12', className)}>
|
||||
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
||||
<div className="absolute -inset-8 -z-[2] backdrop-blur">
|
||||
<Image
|
||||
src={communityCardsImage}
|
||||
fill={true}
|
||||
alt="community-cards"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-background/50 absolute -inset-8 -z-[1] backdrop-blur-[2px]" />
|
||||
|
||||
<div className="relative flex h-full w-full flex-col items-center justify-evenly">
|
||||
<div className="bg-background rounded-2xl border px-4 py-1 text-sm font-medium">
|
||||
User profiles are coming soon!
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{step === 'BASIC_DETAILS' ? (
|
||||
<motion.div className="w-full max-w-md" layoutId="user-profile">
|
||||
<UserProfileTimur
|
||||
rows={2}
|
||||
className="bg-background border-border rounded-2xl border shadow-md"
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div className="w-full max-w-md" layoutId="user-profile">
|
||||
<UserProfileSkeleton
|
||||
user={{ name, url }}
|
||||
rows={2}
|
||||
className="bg-background border-border rounded-2xl border shadow-md"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-border dark:bg-background relative z-10 flex min-h-[min(800px,80vh)] w-full max-w-lg flex-col rounded-xl border bg-neutral-100 p-6">
|
||||
{step === 'BASIC_DETAILS' && (
|
||||
<div className="h-20">
|
||||
<h1 className="text-2xl font-semibold">Create a new account</h1>
|
||||
|
||||
<p className="text-muted-foreground 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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'CLAIM_USERNAME' && (
|
||||
<div className="h-20">
|
||||
<h1 className="text-2xl font-semibold">Claim your username now</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
You will get notified & be able to set up your documenso public profile when we launch
|
||||
the feature.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr className="-mx-6 my-4" />
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex w-full flex-1 flex-col gap-y-4"
|
||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||
>
|
||||
{step === 'BASIC_DETAILS' && (
|
||||
<fieldset
|
||||
className={cn(
|
||||
'flex h-[500px] w-full flex-col gap-y-4',
|
||||
isGoogleSSOEnabled && 'h-[600px]',
|
||||
)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Full Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email Address</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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{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 mt-4 text-sm">
|
||||
Already have an account?{' '}
|
||||
<Link href="/signin" className="text-documenso-700 duration-200 hover:opacity-70">
|
||||
Sign in instead
|
||||
</Link>
|
||||
</p>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{step === 'CLAIM_USERNAME' && (
|
||||
<fieldset
|
||||
className={cn(
|
||||
'flex h-[500px] w-full flex-col gap-y-4',
|
||||
isGoogleSSOEnabled && 'h-[600px]',
|
||||
)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Public profile username</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input type="text" className="mb-2 mt-2 lowercase" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
<div className="bg-muted/50 border-border text-muted-foreground mt-2 inline-block truncate rounded-md border px-2 py-1 text-sm lowercase">
|
||||
{baseUrl.host}/u/{field.value || '<username>'}
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
{step === 'BASIC_DETAILS' && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<span className="font-medium">Basic details</span> 1/2
|
||||
</p>
|
||||
)}
|
||||
|
||||
{step === 'CLAIM_USERNAME' && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<span className="font-medium">Claim username</span> 2/2
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="bg-foreground/40 relative mt-4 h-1.5 rounded-full">
|
||||
<motion.div
|
||||
layout="size"
|
||||
layoutId="document-flow-container-step"
|
||||
className="bg-documenso absolute inset-y-0 left-0 rounded-full"
|
||||
style={{
|
||||
width: step === 'BASIC_DETAILS' ? '50%' : '100%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-4">
|
||||
{/* Go back button, disabled if step is basic details */}
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
disabled={step === 'BASIC_DETAILS'}
|
||||
loading={form.formState.isSubmitting}
|
||||
onClick={() => setStep('BASIC_DETAILS')}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
{/* Continue button */}
|
||||
{step === 'BASIC_DETAILS' && (
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
className="flex-1 disabled:cursor-not-allowed"
|
||||
disabled={!canContinue}
|
||||
loading={form.formState.isSubmitting}
|
||||
onClick={() => setStep('CLAIM_USERNAME')}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Sign up button */}
|
||||
{step === 'CLAIM_USERNAME' && (
|
||||
<Button
|
||||
loading={form.formState.isSubmitting}
|
||||
disabled={!form.formState.isValid}
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="flex-1"
|
||||
>
|
||||
Complete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -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="secondary"
|
||||
variant="ghost"
|
||||
className="w-32"
|
||||
onClick={() => {
|
||||
void router.back();
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { File } from 'lucide-react';
|
||||
import { File, User2 } from 'lucide-react';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { VerifiedIcon } from '@documenso/ui/icons/verified';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export type UserProfileSkeletonProps = {
|
||||
className?: string;
|
||||
user: Pick<User, 'name' | 'url' | 'email'>;
|
||||
user: Pick<User, 'name' | 'url'>;
|
||||
rows?: number;
|
||||
};
|
||||
|
||||
@ -26,28 +24,27 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="border-border bg-background text-muted-foreground inline-block rounded-md border px-2.5 py-1.5 text-sm">
|
||||
{baseUrl.host}/u/{user.url}
|
||||
<div className="border-border bg-background text-muted-foreground inline-flex items-center rounded-md border px-2.5 py-1.5 text-sm">
|
||||
<span>{baseUrl.host}/u/</span>
|
||||
<span className="inline-block max-w-[8rem] truncate lowercase">{user.url}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="bg-primary/40 rounded-full p-2">
|
||||
<Avatar className="h-20 w-20">
|
||||
<AvatarFallback className="bg-primary/80 text-documenso-900 text-xl tracking-wider">
|
||||
{user.name ? extractInitials(user.name) : user.email.slice(0, 1).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="bg-primary/10 rounded-full p-1.5">
|
||||
<div className="bg-background flex h-20 w-20 items-center justify-center rounded-full border-2">
|
||||
<User2 className="h-12 w-12 text-[hsl(228,10%,90%)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<h2 className="text-2xl font-semibold">{user.name}</h2>
|
||||
<div className="flex items-center justify-center gap-x-2">
|
||||
<h2 className="max-w-[12rem] truncate text-2xl font-semibold">{user.name}</h2>
|
||||
|
||||
<VerifiedIcon className="text-primary h-8 w-8" />
|
||||
</div>
|
||||
|
||||
<div className="dark:bg-foreground/20 mx-auto mt-4 h-2 w-52 rounded-full bg-neutral-200" />
|
||||
<div className="dark:bg-foreground/30 mx-auto mt-4 h-2 w-52 rounded-full bg-neutral-300" />
|
||||
<div className="dark:bg-foreground/20 mx-auto mt-2 h-2 w-36 rounded-full bg-neutral-200" />
|
||||
</div>
|
||||
|
||||
@ -68,7 +65,7 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
|
||||
<File className="text-muted-foreground/80 h-8 w-8" strokeWidth={1.5} />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="dark:bg-foreground/20 h-1.5 w-24 rounded-full bg-neutral-200 md:w-36" />
|
||||
<div className="dark:bg-foreground/30 h-1.5 w-24 rounded-full bg-neutral-300 md:w-36" />
|
||||
<div className="dark:bg-foreground/20 h-1.5 w-16 rounded-full bg-neutral-200 md:w-24" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
87
apps/web/src/components/ui/user-profile-timur.tsx
Normal file
87
apps/web/src/components/ui/user-profile-timur.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { File } from 'lucide-react';
|
||||
|
||||
import timurImage from '@documenso/assets/images/timur.png';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { VerifiedIcon } from '@documenso/ui/icons/verified';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export type UserProfileTimurProps = {
|
||||
className?: string;
|
||||
rows?: number;
|
||||
};
|
||||
|
||||
export const UserProfileTimur = ({ className, rows = 2 }: UserProfileTimurProps) => {
|
||||
const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'dark:bg-background flex flex-col items-center rounded-xl bg-neutral-100 p-4',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="border-border bg-background text-muted-foreground inline-block rounded-md border px-2.5 py-1.5 text-sm">
|
||||
{baseUrl.host}/u/timur
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<Image
|
||||
src={timurImage}
|
||||
className="h-20 w-20 rounded-full"
|
||||
alt="image of timur ercan founder of documenso"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center justify-center gap-x-2">
|
||||
<h2 className="text-2xl font-semibold">Timur Ercan</h2>
|
||||
|
||||
<VerifiedIcon className="text-primary h-8 w-8" />
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[40ch] text-center text-sm">Hey I’m Timur</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 max-w-[40ch] text-center text-sm">
|
||||
Pick any of the following agreements below and start signing to get started
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 w-full">
|
||||
<div className="dark:divide-foreground/30 dark:border-foreground/30 divide-y-2 divide-neutral-200 overflow-hidden rounded-lg border-2 border-neutral-200">
|
||||
<div className="text-muted-foreground dark:bg-foreground/20 bg-neutral-50 p-4 font-medium">
|
||||
Documents
|
||||
</div>
|
||||
|
||||
{Array(rows)
|
||||
.fill(0)
|
||||
.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-background flex items-center justify-between gap-x-6 p-4"
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<File className="text-muted-foreground/80 h-8 w-8" strokeWidth={1.5} />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="dark:bg-foreground/30 h-1.5 w-24 rounded-full bg-neutral-300 md:w-36" />
|
||||
<div className="dark:bg-foreground/20 h-1.5 w-16 rounded-full bg-neutral-200 md:w-24" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<Button type="button" size="sm" className="pointer-events-none w-32">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user