feat: update signin signup ui

Signed-off-by: Adithya Krishna <adithya@documenso.com>
This commit is contained in:
Adithya Krishna
2024-02-22 01:57:46 +05:30
committed by Mythie
parent deea6b1535
commit 65d762dd4b
24 changed files with 889 additions and 207 deletions

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
}

View File

@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Braces, CreditCard, Lock, User, Users } from 'lucide-react';
import { Braces, 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';
@ -91,6 +91,19 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
</Button>
</Link>
)}
<Link href="/settings/public-profile">
<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
</Button>
</Link>
</div>
);
};

View File

@ -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>

View File

@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Braces, CreditCard, Lock, User, Users } from 'lucide-react';
import { Braces, 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';
@ -94,6 +94,19 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
</Button>
</Link>
)}
<Link href="/settings/public-profile">
<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
</Button>
</Link>
</div>
);
};

View File

@ -0,0 +1,165 @@
'use client';
import { useRef } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { Copy } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
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 { 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 { Textarea } from '@documenso/ui/primitives/textarea';
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.' }),
profileBio: z
.string()
.max(256, { message: 'Profile bio must not exceed 256 characters' })
.optional(),
});
export type TPublicProfileFormSchema = z.infer<typeof ZPublicProfileFormSchema>;
export type PublicProfileFormProps = {
className?: string;
user: User;
};
export const PublicProfileForm = ({ user, className }: PublicProfileFormProps) => {
const textRef = useRef<HTMLSpanElement>(null);
const { toast } = useToast();
const router = useRouter();
const form = useForm<TPublicProfileFormSchema>({
values: {
profileURL: user.profileURL || '',
},
resolver: zodResolver(ZPublicProfileFormSchema),
});
const isSaving = form.formState.isSubmitting;
const { mutateAsync: updatePublicProfile, data: profileURL } =
trpc.profile.updatePublicProfile.useMutation();
const copyTextToClipboard = async () => {
if (textRef.current) {
try {
await navigator.clipboard.writeText(textRef.current.textContent || '');
} catch (err) {
console.log('Failed to copy: ', err);
}
}
};
const onFormSubmit = async ({ profileURL, profileBio }: TPublicProfileFormSchema) => {
try {
await updatePublicProfile({
profileURL,
profileBio: profileBio || '',
});
toast({
title: 'Public profile updated',
description: 'Your public profile has been updated successfully.',
duration: 5000,
});
router.refresh();
} 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 (
<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={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} />
{profileURL && (
<code className="bg-muted rounded-md px-1 py-1 text-sm">
<span ref={textRef} id="textToCopy">
{profileURL}
</span>
<Button
className="pointer-events-none w-12 px-0"
onClick={copyTextToClipboard}
variant="ghost"
asChild
>
<Copy className="h-8 w-8" />
</Button>
</code>
)}
</>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="profileBio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea className="mt-2" placeholder="Write about yourself..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</fieldset>
<div className="ml-auto mt-4">
<Button type="submit" loading={isSaving}>
{isSaving ? 'Saving changes...' : 'Save changes'}
</Button>
</div>
</form>
</Form>
);
};

View File

@ -2,6 +2,7 @@
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
@ -183,135 +184,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>
);
};