fix: refactor and implement design

This commit is contained in:
Mythie
2024-02-28 14:43:09 +11:00
parent b225cc8139
commit e3e2cfbcfd
42 changed files with 891 additions and 876 deletions

View File

@ -4,7 +4,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import type { DocumentsPageViewProps } from './documents-page-view';
import { DocumentsPageView } from './documents-page-view';
import { PublicProfileIntro } from './username-claim/public-profile-intro';
import { UpcomingProfileClaimTeaser } from './upcoming-profile-claim-teaser';
export type DocumentsPageProps = {
searchParams?: DocumentsPageViewProps['searchParams'];
@ -18,7 +18,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
const { user } = await getRequiredServerComponentSession();
return (
<>
<PublicProfileIntro user={user} />
<UpcomingProfileClaimTeaser user={user} />
<DocumentsPageView searchParams={searchParams} />
</>
);

View File

@ -0,0 +1,52 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import type { User } from '@documenso/prisma/client';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { ClaimPublicProfileDialogForm } from '~/components/forms/public-profile-claim-dialog';
export type UpcomingProfileClaimTeaserProps = {
user: User;
};
export const UpcomingProfileClaimTeaser = ({ user }: UpcomingProfileClaimTeaserProps) => {
const { toast } = useToast();
const [open, setOpen] = useState(false);
const [claimed, setClaimed] = useState(false);
const onOpenChange = useCallback(
(open: boolean) => {
if (!open && !claimed) {
toast({
title: 'Claim your profile later',
description: 'You can claim your profile later on by going to your profile settings!',
});
}
setOpen(open);
localStorage.setItem('app.hasShownProfileClaimDialog', 'true');
},
[claimed, toast],
);
useEffect(() => {
const hasShownProfileClaimDialog =
localStorage.getItem('app.hasShownProfileClaimDialog') === 'true';
if (!user.url && !hasShownProfileClaimDialog) {
onOpenChange(true);
}
}, [onOpenChange, user.url]);
return (
<ClaimPublicProfileDialogForm
open={open}
onOpenChange={onOpenChange}
onClaimed={() => setClaimed(true)}
user={user}
/>
);
};

View File

@ -1,225 +0,0 @@
'use client';
import React, { useRef, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { BadgeCheck, File } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
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(false);
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-xl">
Introducing public profile!
</DialogTitle>
<DialogDescription className="text-center">
Reserve your Documenso public profile username
</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/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 Im 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>
</Card>
</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={showClaimedDialog} 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 <BadgeCheck fill="#A2E771" />
</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>
</>
);
};

View File

@ -0,0 +1,45 @@
'use client';
import { useState } from 'react';
import type { User } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { ClaimPublicProfileDialogForm } from '~/components/forms/public-profile-claim-dialog';
export type ClaimProfileAlertDialogProps = {
className?: string;
user: User;
};
export const ClaimProfileAlertDialog = ({ className, user }: ClaimProfileAlertDialogProps) => {
const [open, setOpen] = useState(false);
return (
<>
<Alert
className={cn(
'flex flex-col items-center justify-between gap-4 p-6 md:flex-row',
className,
)}
variant="neutral"
>
<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
signing revolution.
</AlertDescription>
</div>
<div className="flex-shrink-0">
<Button onClick={() => setOpen(true)}>Claim Now</Button>
</div>
</Alert>
<ClaimPublicProfileDialogForm open={open} onOpenChange={setOpen} user={user} />
</>
);
};

View File

@ -5,6 +5,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { ProfileForm } from '~/components/forms/profile';
import { ClaimProfileAlertDialog } from './claim-profile-alert-dialog';
import { DeleteAccountDialog } from './delete-account-dialog';
export const metadata: Metadata = {
@ -18,9 +19,13 @@ export default async function ProfileSettingsPage() {
<div>
<SettingsHeader title="Profile" subtitle="Here you can edit your personal details." />
<ProfileForm className="max-w-xl" user={user} />
<ProfileForm className="mb-8 max-w-xl" user={user} />
<DeleteAccountDialog className="mt-8 max-w-xl" user={user} />
<ClaimProfileAlertDialog className="max-w-xl" user={user} />
<hr className="my-4 max-w-xl" />
<DeleteAccountDialog className="max-w-xl" user={user} />
</div>
);
}

View File

@ -1,9 +0,0 @@
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>;
}

View File

@ -1,30 +0,0 @@
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>
}
/>
</>
);
}

View File

@ -1,34 +0,0 @@
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>
</>
);
}

View File

@ -9,17 +9,19 @@ export const metadata: Metadata = {
export default function ForgotPasswordPage() {
return (
<div>
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<h1 className="text-4xl font-semibold">Email sent!</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
A password reset email has been sent, if you have an account you should see it in your inbox
shortly.
A password reset email has been sent, if you have an account you should see it in your
inbox shortly.
</p>
<Button asChild>
<Link href="/signin">Return to sign in</Link>
</Button>
</div>
</div>
);
}

View File

@ -1,34 +0,0 @@
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>
</>
);
}

View File

@ -9,7 +9,8 @@ export const metadata: Metadata = {
export default function ForgotPasswordPage() {
return (
<div>
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<h1 className="text-3xl font-semibold">Forgot your password?</h1>
<p className="text-muted-foreground mt-2 text-sm">
@ -26,5 +27,6 @@ export default function ForgotPasswordPage() {
</Link>
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
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="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">
<div>
<div className="absolute -inset-[min(600px,max(400px,60vw))] -z-[1] flex items-center justify-center opacity-70">
<Image
src={backgroundPattern}
alt="background pattern"
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
/>
</div>
<div className="relative w-full">{children}</div>
</div>
</main>
);
}

View File

@ -19,6 +19,7 @@ export default async function ResetPasswordPage({ params: { token } }: ResetPass
}
return (
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<h1 className="text-4xl font-semibold">Reset Password</h1>
@ -33,5 +34,6 @@ export default async function ResetPasswordPage({ params: { token } }: ResetPass
</Link>
</p>
</div>
</div>
);
}

View File

@ -1,34 +0,0 @@
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>
</>
);
}

View File

@ -9,17 +9,19 @@ export const metadata: Metadata = {
export default function ResetPasswordPage() {
return (
<div>
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<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
have still forgotten your password, please request a new reset link.
The token you have used to reset your password is either expired or it never existed. If
you have still forgotten your password, please request a new reset link.
</p>
<Button className="mt-4" asChild>
<Link href="/signin">Return to sign in</Link>
</Button>
</div>
</div>
);
}

View File

@ -1,34 +0,0 @@
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>
</>
);
}

View File

@ -30,31 +30,27 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
}
return (
<>
<div>
<h1 className="text-3xl font-semibold">Sign in to your account</h1>
<div className="w-screen max-w-lg px-4">
<div className="border-border dark:bg-background z-10 rounded-xl border bg-neutral-100 p-6">
<h1 className="text-2xl font-semibold">Sign in to your account</h1>
<p className="text-muted-foreground/60 mt-2 text-sm">
<p className="text-muted-foreground mt-2 text-sm">
Welcome back, we are lucky to have you.
</p>
<hr className="my-4" />
<hr className="-mx-6 my-4" />
<SignInForm
className="mt-0"
initialEmail={email || undefined}
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
/>
<SignInForm initialEmail={email || undefined} isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} />
{process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
{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">
<Link href="/signup" className="text-documenso-700 duration-200 hover:opacity-70">
Sign up
</Link>
</p>
)}
</div>
</>
</div>
);
}

View File

@ -1,26 +0,0 @@
import React from 'react';
import { Card } from '@documenso/ui/primitives/card';
import ClaimUsernameCard from '../../../components/(dashboard)/claim-username-card/claim-username-card';
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 flex flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-[7.2rem]">
<div className="flex w-full items-center gap-x-8">
<ClaimUsernameCard />
<Card className="px-6 py-6">
<div className="w-full">{children}</div>
</Card>
</div>
</main>
</>
);
}

View File

@ -1,13 +1,16 @@
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';
export const metadata: Metadata = {
title: 'Sign Up',
@ -34,18 +37,48 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
}
return (
<>
<h1 className="text-3xl font-semibold">Create a new account</h1>
<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>
<p className="text-muted-foreground/60 mt-2 text-sm">
<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}
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED || true}
/>
<p className="text-muted-foreground mt-6 text-center text-sm">
@ -54,6 +87,7 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
Sign in instead
</Link>
</p>
</>
</div>
</div>
);
}

View File

@ -29,7 +29,8 @@ export default async function AcceptInvitationPage({
if (!teamMemberInvite) {
return (
<div>
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<h1 className="text-4xl font-semibold">Invalid token</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
@ -40,6 +41,7 @@ export default async function AcceptInvitationPage({
<Link href="/">Return</Link>
</Button>
</div>
</div>
);
}

View File

@ -1,34 +0,0 @@
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>
</>
);
}

View File

@ -22,7 +22,8 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
return (
<div>
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<h1 className="text-4xl font-semibold">Invalid link</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
@ -33,6 +34,7 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
<Link href="/">Return</Link>
</Button>
</div>
</div>
);
}

View File

@ -25,7 +25,8 @@ export default async function VerifyTeamTransferPage({
if (!teamTransferVerification || isTokenExpired(teamTransferVerification.expiresAt)) {
return (
<div>
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<h1 className="text-4xl font-semibold">Invalid link</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
@ -37,6 +38,7 @@ export default async function VerifyTeamTransferPage({
<Link href="/">Return</Link>
</Button>
</div>
</div>
);
}

View File

@ -4,7 +4,8 @@ import { SendConfirmationEmailForm } from '~/components/forms/send-confirmation-
export default function UnverifiedAccount() {
return (
<div className="flex w-full items-start">
<div className="w-screen max-w-lg px-4">
<div className="flex items-start">
<div className="mr-4 mt-1 hidden md:block">
<Mails className="text-primary h-10 w-10" strokeWidth={2} />
</div>
@ -23,5 +24,6 @@ export default function UnverifiedAccount() {
<SendConfirmationEmailForm />
</div>
</div>
</div>
);
}

View File

@ -14,6 +14,7 @@ export type PageProps = {
export default async function VerifyEmailPage({ params: { token } }: PageProps) {
if (!token) {
return (
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<div className="mb-4 text-red-300">
<XOctagon />
@ -24,6 +25,7 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
It seems that there is no token provided. Please check your email and try again.
</p>
</div>
</div>
);
}
@ -31,6 +33,7 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
if (verified === null) {
return (
<div className="w-screen max-w-lg px-4">
<div className="flex w-full items-start">
<div className="mr-4 mt-1 hidden md:block">
<AlertTriangle className="h-10 w-10 text-yellow-500" strokeWidth={2} />
@ -49,11 +52,13 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
</Button>
</div>
</div>
</div>
);
}
if (!verified) {
return (
<div className="w-screen max-w-lg px-4">
<div className="flex w-full items-start">
<div className="mr-4 mt-1 hidden md:block">
<XCircle className="text-destructive h-10 w-10" strokeWidth={2} />
@ -63,8 +68,8 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
<h2 className="text-2xl font-bold md:text-4xl">Your token has expired!</h2>
<p className="text-muted-foreground mt-4">
It seems that the provided token has expired. We've just sent you another token, please
check your email and try again.
It seems that the provided token has expired. We've just sent you another token,
please check your email and try again.
</p>
<Button className="mt-4" asChild>
@ -72,10 +77,12 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
</Button>
</div>
</div>
</div>
);
}
return (
<div className="w-screen max-w-lg px-4">
<div className="flex w-full items-start">
<div className="mr-4 mt-1 hidden md:block">
<CheckCircle2 className="h-10 w-10 text-green-500" strokeWidth={2} />
@ -93,5 +100,6 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
</Button>
</div>
</div>
</div>
);
}

View File

@ -1,34 +0,0 @@
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>
</>
);
}

View File

@ -11,13 +11,16 @@ export const metadata: Metadata = {
export default function EmailVerificationWithoutTokenPage() {
return (
<div className="w-screen max-w-lg px-4">
<div className="flex w-full items-start">
<div className="mr-4 mt-1 hidden md:block">
<XCircle className="text-destructive h-10 w-10" strokeWidth={2} />
</div>
<div>
<h2 className="text-2xl font-bold md:text-4xl">Uh oh! Looks like you're missing a token</h2>
<h2 className="text-2xl font-bold md:text-4xl">
Uh oh! Looks like you're missing a token
</h2>
<p className="text-muted-foreground mt-4">
It seems that there is no token provided, if you are trying to verify your email please
@ -29,5 +32,6 @@ export default function EmailVerificationWithoutTokenPage() {
</Button>
</div>
</div>
</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, Globe2, Lock, User, Users } from 'lucide-react';
import { Braces, CreditCard, Lock, User, Users } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
@ -91,25 +91,6 @@ 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>
);
};

View File

@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Braces, CreditCard, Globe2, Lock, User, Users } from 'lucide-react';
import { Braces, CreditCard, Lock, User, Users } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
@ -94,25 +94,6 @@ 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>
);
};

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

View File

@ -184,7 +184,6 @@ 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)}
@ -197,9 +196,11 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
<Input type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
@ -211,19 +212,19 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<>
<PasswordInput placeholder="Password" {...field} />
<p className="mt-2.5 text-right">
<PasswordInput {...field} />
</FormControl>
<p className="mt-2 text-right">
<Link
href="/forgot-password"
className="text-muted-foreground/60 text-sm duration-200 hover:opacity-70"
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
>
Forgot your password?
</Link>
</p>
</>
</FormControl>
<FormMessage />
</FormItem>
)}
@ -325,6 +326,5 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
</DialogContent>
</Dialog>
</Form>
</div>
);
};

View File

@ -0,0 +1,87 @@
'use client';
import { File } 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'>;
rows?: number;
};
export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSkeletonProps) => {
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/{user.url}
</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>
</div>
<div className="mt-6">
<div className="flex items-center gap-x-2">
<h2 className="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/20 mx-auto mt-2 h-2 w-36 rounded-full bg-neutral-200" />
</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/20 h-1.5 w-24 rounded-full bg-neutral-200 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>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -18,6 +18,7 @@ export enum AppErrorCode {
'RETRY_EXCEPTION' = 'RetryException',
'SCHEMA_FAILED' = 'SchemaFailed',
'TOO_MANY_REQUESTS' = 'TooManyRequests',
'PROFILE_URL_TAKEN' = 'ProfileUrlTaken',
}
const genericErrorCodeToTrpcErrorCodeMap: Record<string, TRPCError['code']> = {
@ -32,6 +33,7 @@ const genericErrorCodeToTrpcErrorCodeMap: Record<string, TRPCError['code']> = {
[AppErrorCode.RETRY_EXCEPTION]: 'INTERNAL_SERVER_ERROR',
[AppErrorCode.SCHEMA_FAILED]: 'INTERNAL_SERVER_ERROR',
[AppErrorCode.TOO_MANY_REQUESTS]: 'TOO_MANY_REQUESTS',
[AppErrorCode.PROFILE_URL_TAKEN]: 'BAD_REQUEST',
};
export const ZAppErrorJsonSchema = z.object({

View File

@ -1,35 +1,49 @@
import { prisma } from '@documenso/prisma';
import type { User, UserProfile } from '@documenso/prisma/client';
import { getUserById } from './get-user-by-id';
import { AppError, AppErrorCode } from '../../errors/app-error';
export type UpdatePublicProfileOptions = {
id: User['id'];
profileURL: UserProfile['profileURL'];
userId: number;
url: string;
};
export const updatePublicProfile = async ({ id, profileURL }: UpdatePublicProfileOptions) => {
const user = await getUserById({ id });
// Existence check
await prisma.userProfile.findFirstOrThrow({
export const updatePublicProfile = async ({ userId, url }: UpdatePublicProfileOptions) => {
const isUrlTaken = await prisma.user.findFirst({
select: {
id: true,
},
where: {
profileURL: user.profileURL ?? undefined,
id: {
not: userId,
},
url,
},
});
return await prisma.$transaction(async (tx) => {
await tx.userProfile.create({
data: {
profileURL,
},
});
await tx.userProfile.update({
if (isUrlTaken) {
throw new AppError(
AppErrorCode.PROFILE_URL_TAKEN,
'Profile URL is taken',
'The profile URL is already taken',
);
}
return await prisma.user.update({
where: {
profileURL: user.profileURL ?? undefined,
id: userId,
},
data: {
profileURL: profileURL,
url,
userProfile: {
upsert: {
create: {
bio: '',
},
update: {
bio: '',
},
},
},
},
});
});
};

View File

@ -0,0 +1,37 @@
/*
Warnings:
- You are about to drop the column `profileURL` on the `User` table. All the data in the column will be lost.
- The primary key for the `UserProfile` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `profileBio` on the `UserProfile` table. All the data in the column will be lost.
- You are about to drop the column `profileURL` on the `UserProfile` table. All the data in the column will be lost.
- A unique constraint covering the columns `[url]` on the table `User` will be added. If there are existing duplicate values, this will fail.
- Added the required column `id` to the `UserProfile` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "User" DROP CONSTRAINT "User_profileURL_fkey";
-- DropIndex
DROP INDEX "User_profileURL_key";
-- DropIndex
DROP INDEX "UserProfile_profileURL_key";
-- AlterTable
ALTER TABLE "User" DROP COLUMN "profileURL",
ADD COLUMN "url" TEXT;
-- AlterTable
ALTER TABLE "UserProfile" DROP CONSTRAINT "UserProfile_pkey",
DROP COLUMN "profileBio",
DROP COLUMN "profileURL",
ADD COLUMN "bio" TEXT,
ADD COLUMN "id" INTEGER NOT NULL,
ADD CONSTRAINT "UserProfile_pkey" PRIMARY KEY ("id");
-- CreateIndex
CREATE UNIQUE INDEX "User_url_key" ON "User"("url");
-- AddForeignKey
ALTER TABLE "UserProfile" ADD CONSTRAINT "UserProfile_id_fkey" FOREIGN KEY ("id") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -43,9 +43,9 @@ model User {
twoFactorSecret String?
twoFactorEnabled Boolean @default(false)
twoFactorBackupCodes String?
profileURL String? @unique
url String? @unique
UserProfile UserProfile? @relation(fields: [profileURL], references: [profileURL], onDelete: Cascade)
userProfile UserProfile?
VerificationToken VerificationToken[]
ApiToken ApiToken[]
Template Template[]
@ -56,10 +56,10 @@ model User {
}
model UserProfile {
profileURL String @id @unique
profileBio String?
id Int @id
bio String?
User User?
User User? @relation(fields: [id], references: [id], onDelete: Cascade)
}
enum UserSecurityAuditLogType {

View File

@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
@ -80,14 +81,20 @@ export const profileRouter = router({
.input(ZUpdatePublicProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { profileURL } = input;
const { url } = input;
return await updatePublicProfile({
id: ctx.user.id,
profileURL,
const user = await updatePublicProfile({
userId: ctx.user.id,
url,
});
return { success: true, url: user.url };
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
throw AppError.parseErrorToTRPCError(error);
}
throw new TRPCError({
code: 'BAD_REQUEST',

View File

@ -17,7 +17,7 @@ export const ZUpdateProfileMutationSchema = z.object({
});
export const ZUpdatePublicProfileMutationSchema = z.object({
profileURL: z.string().min(1),
url: z.string().min(1),
});
export const ZUpdatePasswordMutationSchema = z.object({

View File

@ -0,0 +1,31 @@
import { forwardRef } from 'react';
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
export const VerifiedIcon: LucideIcon = forwardRef(
({ size = 24, color = 'currentColor', ...props }, ref) => {
return (
<svg
ref={ref}
width={size}
height={size}
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g id="badge, verified, award">
<path
id="Icon"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.5457 2.89094C11.5779 1.70302 13.4223 1.70302 14.4545 2.89094L15.2585 3.81628C15.3917 3.96967 15.5947 4.04354 15.7954 4.0117L17.0061 3.81965C18.5603 3.57309 19.9732 4.75869 20.0003 6.33214L20.0214 7.55778C20.0249 7.76096 20.1329 7.94799 20.3071 8.05261L21.358 8.6837C22.7071 9.49389 23.0274 11.3103 22.0368 12.5331L21.2651 13.4855C21.1372 13.6434 21.0997 13.8561 21.1659 14.0482L21.5652 15.2072C22.0779 16.695 21.1557 18.2923 19.6109 18.5922L18.4075 18.8258C18.208 18.8646 18.0426 19.0034 17.9698 19.1931L17.5308 20.3376C16.9672 21.8069 15.234 22.4378 13.8578 21.6745L12.7858 21.08C12.6081 20.9814 12.3921 20.9814 12.2144 21.08L11.1424 21.6745C9.76623 22.4378 8.033 21.8069 7.4694 20.3376L7.03038 19.1931C6.9576 19.0034 6.79216 18.8646 6.59268 18.8258L5.38932 18.5922C3.84448 18.2923 2.92224 16.695 3.43495 15.2072L3.83431 14.0482C3.90052 13.8561 3.86302 13.6434 3.7351 13.4855L2.96343 12.5331C1.97279 11.3103 2.29307 9.49389 3.64218 8.6837L4.69306 8.05261C4.86728 7.94799 4.97526 7.76096 4.97875 7.55778L4.99985 6.33214C5.02694 4.75869 6.43987 3.57309 7.99413 3.81965L9.20481 4.0117C9.40551 4.04354 9.60845 3.96967 9.74173 3.81628L10.5457 2.89094ZM15.7072 11.2071C16.0977 10.8166 16.0977 10.1834 15.7072 9.79289C15.3167 9.40237 14.6835 9.40237 14.293 9.79289L11.5001 12.5858L10.7072 11.7929C10.3167 11.4024 9.68351 11.4024 9.29298 11.7929C8.90246 12.1834 8.90246 12.8166 9.29298 13.2071L10.4394 14.3536C11.0252 14.9393 11.975 14.9393 12.5608 14.3536L15.7072 11.2071Z"
fill={color}
/>
</g>
</svg>
);
},
);
VerifiedIcon.displayName = 'VerifiedIcon';

View File

@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'bg-background border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
{
'ring-2 !ring-red-500 transition-all': props['aria-invalid'],