mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 17:21:41 +10:00
fix: refactor and implement design
This commit is contained in:
182
apps/web/src/components/forms/public-profile-claim-dialog.tsx
Normal file
182
apps/web/src/components/forms/public-profile-claim-dialog.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
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 { 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.' }),
|
||||
});
|
||||
|
||||
export type TClaimPublicProfileFormSchema = z.infer<typeof ZClaimPublicProfileFormSchema>;
|
||||
|
||||
export type ClaimPublicProfileDialogFormProps = {
|
||||
open: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
onClaimed?: () => void;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const ClaimPublicProfileDialogForm = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onClaimed,
|
||||
user,
|
||||
}: ClaimPublicProfileDialogFormProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const [claimed, setClaimed] = useState(false);
|
||||
|
||||
const form = useForm<TClaimPublicProfileFormSchema>({
|
||||
values: {
|
||||
url: user.url || '',
|
||||
},
|
||||
resolver: zodResolver(ZClaimPublicProfileFormSchema),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePublicProfile } = trpc.profile.updatePublicProfile.useMutation();
|
||||
|
||||
const isSubmitting = form.formState.isSubmitting;
|
||||
|
||||
const onFormSubmit = async ({ url }: TClaimPublicProfileFormSchema) => {
|
||||
try {
|
||||
await updatePublicProfile({
|
||||
url,
|
||||
});
|
||||
|
||||
setClaimed(true);
|
||||
onClaimed?.();
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
|
||||
form.setError('url', {
|
||||
type: 'manual',
|
||||
message: 'This URL is already taken',
|
||||
});
|
||||
} else if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
|
||||
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={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent position="center" className="max-w-lg overflow-hidden">
|
||||
{!claimed && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-semi-bold text-center text-xl">
|
||||
Introducing public profiles!
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription className="text-center">
|
||||
Reserve your Documenso public profile username
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Image src={profileClaimTeaserImage} alt="profile claim teaser" />
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={cn(
|
||||
'to-background -mt-32 flex w-full flex-col bg-gradient-to-b from-transparent to-15% pt-16 md:-mt-44',
|
||||
)}
|
||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<fieldset className="-mt-6 flex w-full flex-col gap-y-4" disabled={isSubmitting}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Public profile URL</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input type="text" className="mb-2 mt-2" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<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>'}
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
Claim your username
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
|
||||
{claimed && (
|
||||
<>
|
||||
<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 features is launched
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<UserProfileSkeleton className="mt-4" user={user} rows={1} />
|
||||
|
||||
<div className="to-background -mt-12 flex w-full flex-col items-center bg-gradient-to-b from-transparent to-15% px-4 pt-8 md:-mt-12">
|
||||
<Button className="w-full" onClick={() => onOpenChange?.(false)}>
|
||||
Can't wait!
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -184,147 +184,147 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('mt-1')}>
|
||||
<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" placeholder="john@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso dark:hover:opacity-90"
|
||||
>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
<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 continue with</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
|
||||
<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>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
|
||||
<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}
|
||||
<p className="mt-2 text-right">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||
>
|
||||
{twoFactorAuthenticationMethod === 'totp'
|
||||
? 'Use Backup Code'
|
||||
: 'Use Authenticator'}
|
||||
</Button>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Form>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso dark:hover:opacity-90"
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user