mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
fix: refactor and implement design
This commit is contained in:
@ -4,7 +4,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
|||||||
|
|
||||||
import type { DocumentsPageViewProps } from './documents-page-view';
|
import type { DocumentsPageViewProps } from './documents-page-view';
|
||||||
import { DocumentsPageView } 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 = {
|
export type DocumentsPageProps = {
|
||||||
searchParams?: DocumentsPageViewProps['searchParams'];
|
searchParams?: DocumentsPageViewProps['searchParams'];
|
||||||
@ -18,7 +18,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PublicProfileIntro user={user} />
|
<UpcomingProfileClaimTeaser user={user} />
|
||||||
<DocumentsPageView searchParams={searchParams} />
|
<DocumentsPageView searchParams={searchParams} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 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>
|
|
||||||
</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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -5,6 +5,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
|||||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||||
import { ProfileForm } from '~/components/forms/profile';
|
import { ProfileForm } from '~/components/forms/profile';
|
||||||
|
|
||||||
|
import { ClaimProfileAlertDialog } from './claim-profile-alert-dialog';
|
||||||
import { DeleteAccountDialog } from './delete-account-dialog';
|
import { DeleteAccountDialog } from './delete-account-dialog';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@ -18,9 +19,13 @@ export default async function ProfileSettingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<SettingsHeader title="Profile" subtitle="Here you can edit your personal details." />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>;
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -9,17 +9,19 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function ForgotPasswordPage() {
|
export default function ForgotPasswordPage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="w-screen max-w-lg px-4">
|
||||||
<h1 className="text-4xl font-semibold">Email sent!</h1>
|
<div className="w-full">
|
||||||
|
<h1 className="text-4xl font-semibold">Email sent!</h1>
|
||||||
|
|
||||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
<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
|
A password reset email has been sent, if you have an account you should see it in your
|
||||||
shortly.
|
inbox shortly.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/signin">Return to sign in</Link>
|
<Link href="/signin">Return to sign in</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -9,22 +9,24 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function ForgotPasswordPage() {
|
export default function ForgotPasswordPage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="w-screen max-w-lg px-4">
|
||||||
<h1 className="text-3xl font-semibold">Forgot your password?</h1>
|
<div className="w-full">
|
||||||
|
<h1 className="text-3xl font-semibold">Forgot your password?</h1>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
No worries, it happens! Enter your email and we'll email you a special link to reset your
|
No worries, it happens! Enter your email and we'll email you a special link to reset your
|
||||||
password.
|
password.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ForgotPasswordForm className="mt-4" />
|
<ForgotPasswordForm className="mt-4" />
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||||
Remembered your password?{' '}
|
Remembered your password?{' '}
|
||||||
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
|
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
|
||||||
Sign In
|
Sign In
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
27
apps/web/src/app/(unauthenticated)/layout.tsx
Normal file
27
apps/web/src/app/(unauthenticated)/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -19,19 +19,21 @@ export default async function ResetPasswordPage({ params: { token } }: ResetPass
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-screen max-w-lg px-4">
|
||||||
<h1 className="text-4xl font-semibold">Reset Password</h1>
|
<div className="w-full">
|
||||||
|
<h1 className="text-4xl font-semibold">Reset Password</h1>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">Please choose your new password </p>
|
<p className="text-muted-foreground mt-2 text-sm">Please choose your new password </p>
|
||||||
|
|
||||||
<ResetPasswordForm token={token} className="mt-4" />
|
<ResetPasswordForm token={token} className="mt-4" />
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
<Link href="/signup" className="text-primary duration-200 hover:opacity-70">
|
<Link href="/signup" className="text-primary duration-200 hover:opacity-70">
|
||||||
Sign up
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -9,17 +9,19 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function ResetPasswordPage() {
|
export default function ResetPasswordPage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="w-screen max-w-lg px-4">
|
||||||
<h1 className="text-3xl font-semibold">Unable to reset password</h1>
|
<div className="w-full">
|
||||||
|
<h1 className="text-3xl font-semibold">Unable to reset password</h1>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<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
|
The token you have used to reset your password is either expired or it never existed. If
|
||||||
have still forgotten your password, please request a new reset link.
|
you have still forgotten your password, please request a new reset link.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button className="mt-4" asChild>
|
<Button className="mt-4" asChild>
|
||||||
<Link href="/signin">Return to sign in</Link>
|
<Link href="/signin">Return to sign in</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -30,31 +30,27 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-screen max-w-lg px-4">
|
||||||
<div>
|
<div className="border-border dark:bg-background z-10 rounded-xl border bg-neutral-100 p-6">
|
||||||
<h1 className="text-3xl font-semibold">Sign in to your account</h1>
|
<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.
|
Welcome back, we are lucky to have you.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="my-4" />
|
<hr className="-mx-6 my-4" />
|
||||||
|
|
||||||
<SignInForm
|
<SignInForm initialEmail={email || undefined} isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} />
|
||||||
className="mt-0"
|
|
||||||
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">
|
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||||
Don't have an account?{' '}
|
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
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,13 +1,16 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { env } from 'next-runtime-env';
|
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 { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||||
|
|
||||||
import { SignUpForm } from '~/components/forms/signup';
|
import { SignUpForm } from '~/components/forms/signup';
|
||||||
|
import { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Sign Up',
|
title: 'Sign Up',
|
||||||
@ -34,26 +37,57 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex w-screen max-w-screen-2xl justify-center gap-x-12 px-4 md:px-16">
|
||||||
<h1 className="text-3xl font-semibold">Create a new account</h1>
|
<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]" />
|
||||||
Create your account and start using state-of-the-art document signing. Open and beautiful
|
|
||||||
signing is within your grasp.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<SignUpForm
|
<div className="relative flex h-full w-full flex-col items-center justify-evenly">
|
||||||
className="mt-1"
|
<div className="bg-background rounded-2xl border px-4 py-1 text-sm font-medium">
|
||||||
initialEmail={email || undefined}
|
User profiles are coming soon!
|
||||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
</div>
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
<UserProfileSkeleton
|
||||||
Already have an account?{' '}
|
user={{ name: 'Timur Ercan', email: 'timur@documenso.com', url: 'timur' }}
|
||||||
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
|
rows={2}
|
||||||
Sign in instead
|
className="bg-background border-border w-full max-w-md rounded-2xl border shadow-md"
|
||||||
</Link>
|
/>
|
||||||
</p>
|
|
||||||
</>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,16 +29,18 @@ export default async function AcceptInvitationPage({
|
|||||||
|
|
||||||
if (!teamMemberInvite) {
|
if (!teamMemberInvite) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="w-screen max-w-lg px-4">
|
||||||
<h1 className="text-4xl font-semibold">Invalid token</h1>
|
<div className="w-full">
|
||||||
|
<h1 className="text-4xl font-semibold">Invalid token</h1>
|
||||||
|
|
||||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||||
This token is invalid or has expired. Please contact your team for a new invitation.
|
This token is invalid or has expired. Please contact your team for a new invitation.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/">Return</Link>
|
<Link href="/">Return</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -22,16 +22,18 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
|
|||||||
|
|
||||||
if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
|
if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="w-screen max-w-lg px-4">
|
||||||
<h1 className="text-4xl font-semibold">Invalid link</h1>
|
<div className="w-full">
|
||||||
|
<h1 className="text-4xl font-semibold">Invalid link</h1>
|
||||||
|
|
||||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||||
This link is invalid or has expired. Please contact your team to resend a verification.
|
This link is invalid or has expired. Please contact your team to resend a verification.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/">Return</Link>
|
<Link href="/">Return</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,17 +25,19 @@ export default async function VerifyTeamTransferPage({
|
|||||||
|
|
||||||
if (!teamTransferVerification || isTokenExpired(teamTransferVerification.expiresAt)) {
|
if (!teamTransferVerification || isTokenExpired(teamTransferVerification.expiresAt)) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="w-screen max-w-lg px-4">
|
||||||
<h1 className="text-4xl font-semibold">Invalid link</h1>
|
<div className="w-full">
|
||||||
|
<h1 className="text-4xl font-semibold">Invalid link</h1>
|
||||||
|
|
||||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||||
This link is invalid or has expired. Please contact your team to resend a transfer
|
This link is invalid or has expired. Please contact your team to resend a transfer
|
||||||
request.
|
request.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/">Return</Link>
|
<Link href="/">Return</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,23 +4,25 @@ import { SendConfirmationEmailForm } from '~/components/forms/send-confirmation-
|
|||||||
|
|
||||||
export default function UnverifiedAccount() {
|
export default function UnverifiedAccount() {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-start">
|
<div className="w-screen max-w-lg px-4">
|
||||||
<div className="mr-4 mt-1 hidden md:block">
|
<div className="flex items-start">
|
||||||
<Mails className="text-primary h-10 w-10" strokeWidth={2} />
|
<div className="mr-4 mt-1 hidden md:block">
|
||||||
</div>
|
<Mails className="text-primary h-10 w-10" strokeWidth={2} />
|
||||||
<div className="">
|
</div>
|
||||||
<h2 className="text-2xl font-bold md:text-4xl">Confirm email</h2>
|
<div className="">
|
||||||
|
<h2 className="text-2xl font-bold md:text-4xl">Confirm email</h2>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">
|
<p className="text-muted-foreground mt-4">
|
||||||
To gain access to your account, please confirm your email address by clicking on the
|
To gain access to your account, please confirm your email address by clicking on the
|
||||||
confirmation link from your inbox.
|
confirmation link from your inbox.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">
|
<p className="text-muted-foreground mt-4">
|
||||||
If you don't find the confirmation link in your inbox, you can request a new one below.
|
If you don't find the confirmation link in your inbox, you can request a new one below.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SendConfirmationEmailForm />
|
<SendConfirmationEmailForm />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,15 +14,17 @@ export type PageProps = {
|
|||||||
export default async function VerifyEmailPage({ params: { token } }: PageProps) {
|
export default async function VerifyEmailPage({ params: { token } }: PageProps) {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-screen max-w-lg px-4">
|
||||||
<div className="mb-4 text-red-300">
|
<div className="w-full">
|
||||||
<XOctagon />
|
<div className="mb-4 text-red-300">
|
||||||
</div>
|
<XOctagon />
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 className="text-4xl font-semibold">No token provided</h2>
|
<h2 className="text-4xl font-semibold">No token provided</h2>
|
||||||
<p className="text-muted-foreground mt-2 text-base">
|
<p className="text-muted-foreground mt-2 text-base">
|
||||||
It seems that there is no token provided. Please check your email and try again.
|
It seems that there is no token provided. Please check your email and try again.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -31,22 +33,24 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
|
|||||||
|
|
||||||
if (verified === null) {
|
if (verified === null) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-start">
|
<div className="w-screen max-w-lg px-4">
|
||||||
<div className="mr-4 mt-1 hidden md:block">
|
<div className="flex w-full items-start">
|
||||||
<AlertTriangle className="h-10 w-10 text-yellow-500" strokeWidth={2} />
|
<div className="mr-4 mt-1 hidden md:block">
|
||||||
</div>
|
<AlertTriangle className="h-10 w-10 text-yellow-500" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold md:text-4xl">Something went wrong</h2>
|
<h2 className="text-2xl font-bold md:text-4xl">Something went wrong</h2>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">
|
<p className="text-muted-foreground mt-4">
|
||||||
We were unable to verify your email. If your email is not verified already, please try
|
We were unable to verify your email. If your email is not verified already, please try
|
||||||
again.
|
again.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button className="mt-4" asChild>
|
<Button className="mt-4" asChild>
|
||||||
<Link href="/">Go back home</Link>
|
<Link href="/">Go back home</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -54,17 +58,41 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
|
|||||||
|
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
return (
|
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">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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button className="mt-4" asChild>
|
||||||
|
<Link href="/">Go back home</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen max-w-lg px-4">
|
||||||
<div className="flex w-full items-start">
|
<div className="flex w-full items-start">
|
||||||
<div className="mr-4 mt-1 hidden md:block">
|
<div className="mr-4 mt-1 hidden md:block">
|
||||||
<XCircle className="text-destructive h-10 w-10" strokeWidth={2} />
|
<CheckCircle2 className="h-10 w-10 text-green-500" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold md:text-4xl">Your token has expired!</h2>
|
<h2 className="text-2xl font-bold md:text-4xl">Email Confirmed!</h2>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">
|
<p className="text-muted-foreground mt-4">
|
||||||
It seems that the provided token has expired. We've just sent you another token, please
|
Your email has been successfully confirmed! You can now use all features of Documenso.
|
||||||
check your email and try again.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button className="mt-4" asChild>
|
<Button className="mt-4" asChild>
|
||||||
@ -72,26 +100,6 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-bold md:text-4xl">Email Confirmed!</h2>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">
|
|
||||||
Your email has been successfully confirmed! You can now use all features of Documenso.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Button className="mt-4" asChild>
|
|
||||||
<Link href="/">Go back home</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -11,22 +11,26 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function EmailVerificationWithoutTokenPage() {
|
export default function EmailVerificationWithoutTokenPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-start">
|
<div className="w-screen max-w-lg px-4">
|
||||||
<div className="mr-4 mt-1 hidden md:block">
|
<div className="flex w-full items-start">
|
||||||
<XCircle className="text-destructive h-10 w-10" strokeWidth={2} />
|
<div className="mr-4 mt-1 hidden md:block">
|
||||||
</div>
|
<XCircle className="text-destructive h-10 w-10" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">
|
<p className="text-muted-foreground mt-4">
|
||||||
It seems that there is no token provided, if you are trying to verify your email please
|
It seems that there is no token provided, if you are trying to verify your email please
|
||||||
follow the link in your email.
|
follow the link in your email.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button className="mt-4" asChild>
|
<Button className="mt-4" asChild>
|
||||||
<Link href="/">Go back home</Link>
|
<Link href="/">Go back home</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
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 { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -91,25 +91,6 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
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 { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -94,25 +94,6 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
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 (
|
return (
|
||||||
<div className={cn('mt-1')}>
|
<Form {...form}>
|
||||||
<Form {...form}>
|
<form
|
||||||
<form
|
className={cn('flex w-full flex-col gap-y-4', className)}
|
||||||
className={cn('flex w-full flex-col gap-y-4', className)}
|
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
>
|
||||||
>
|
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}>
|
||||||
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}>
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="email"
|
||||||
name="email"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormLabel>Email</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="email" placeholder="john@example.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Input type="email" {...field} />
|
||||||
name="password"
|
</FormControl>
|
||||||
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>
|
|
||||||
|
|
||||||
<Button
|
<FormMessage />
|
||||||
type="submit"
|
</FormItem>
|
||||||
size="lg"
|
)}
|
||||||
loading={isSubmitting}
|
/>
|
||||||
className="dark:bg-documenso dark:hover:opacity-90"
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{isGoogleSSOEnabled && (
|
<FormField
|
||||||
<>
|
control={form.control}
|
||||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
name="password"
|
||||||
<div className="bg-border h-px flex-1" />
|
render={({ field }) => (
|
||||||
<span className="text-muted-foreground bg-transparent">Or continue with</span>
|
<FormItem>
|
||||||
<div className="bg-border h-px flex-1" />
|
<FormLabel>Password</FormLabel>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<FormControl>
|
||||||
type="button"
|
<PasswordInput {...field} />
|
||||||
size="lg"
|
</FormControl>
|
||||||
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
|
<p className="mt-2 text-right">
|
||||||
open={isTwoFactorAuthenticationDialogOpen}
|
<Link
|
||||||
onOpenChange={onCloseTwoFactorAuthenticationDialog}
|
href="/forgot-password"
|
||||||
>
|
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||||
<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'
|
Forgot your password?
|
||||||
? 'Use Backup Code'
|
</Link>
|
||||||
: 'Use Authenticator'}
|
</p>
|
||||||
</Button>
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<Button type="submit" loading={isSubmitting}>
|
<Button
|
||||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
type="submit"
|
||||||
</Button>
|
size="lg"
|
||||||
</DialogFooter>
|
loading={isSubmitting}
|
||||||
</fieldset>
|
className="dark:bg-documenso dark:hover:opacity-90"
|
||||||
</form>
|
>
|
||||||
</DialogContent>
|
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||||
</Dialog>
|
</Button>
|
||||||
</Form>
|
|
||||||
</div>
|
{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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
87
apps/web/src/components/ui/user-profile-skeleton.tsx
Normal file
87
apps/web/src/components/ui/user-profile-skeleton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
BIN
packages/assets/images/community-cards.png
Normal file
BIN
packages/assets/images/community-cards.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 MiB |
BIN
packages/assets/images/profile-claim-teaser.png
Normal file
BIN
packages/assets/images/profile-claim-teaser.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@ -18,6 +18,7 @@ export enum AppErrorCode {
|
|||||||
'RETRY_EXCEPTION' = 'RetryException',
|
'RETRY_EXCEPTION' = 'RetryException',
|
||||||
'SCHEMA_FAILED' = 'SchemaFailed',
|
'SCHEMA_FAILED' = 'SchemaFailed',
|
||||||
'TOO_MANY_REQUESTS' = 'TooManyRequests',
|
'TOO_MANY_REQUESTS' = 'TooManyRequests',
|
||||||
|
'PROFILE_URL_TAKEN' = 'ProfileUrlTaken',
|
||||||
}
|
}
|
||||||
|
|
||||||
const genericErrorCodeToTrpcErrorCodeMap: Record<string, TRPCError['code']> = {
|
const genericErrorCodeToTrpcErrorCodeMap: Record<string, TRPCError['code']> = {
|
||||||
@ -32,6 +33,7 @@ const genericErrorCodeToTrpcErrorCodeMap: Record<string, TRPCError['code']> = {
|
|||||||
[AppErrorCode.RETRY_EXCEPTION]: 'INTERNAL_SERVER_ERROR',
|
[AppErrorCode.RETRY_EXCEPTION]: 'INTERNAL_SERVER_ERROR',
|
||||||
[AppErrorCode.SCHEMA_FAILED]: 'INTERNAL_SERVER_ERROR',
|
[AppErrorCode.SCHEMA_FAILED]: 'INTERNAL_SERVER_ERROR',
|
||||||
[AppErrorCode.TOO_MANY_REQUESTS]: 'TOO_MANY_REQUESTS',
|
[AppErrorCode.TOO_MANY_REQUESTS]: 'TOO_MANY_REQUESTS',
|
||||||
|
[AppErrorCode.PROFILE_URL_TAKEN]: 'BAD_REQUEST',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZAppErrorJsonSchema = z.object({
|
export const ZAppErrorJsonSchema = z.object({
|
||||||
|
|||||||
@ -1,35 +1,49 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
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 = {
|
export type UpdatePublicProfileOptions = {
|
||||||
id: User['id'];
|
userId: number;
|
||||||
profileURL: UserProfile['profileURL'];
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updatePublicProfile = async ({ id, profileURL }: UpdatePublicProfileOptions) => {
|
export const updatePublicProfile = async ({ userId, url }: UpdatePublicProfileOptions) => {
|
||||||
const user = await getUserById({ id });
|
const isUrlTaken = await prisma.user.findFirst({
|
||||||
// Existence check
|
select: {
|
||||||
await prisma.userProfile.findFirstOrThrow({
|
id: true,
|
||||||
|
},
|
||||||
where: {
|
where: {
|
||||||
profileURL: user.profileURL ?? undefined,
|
id: {
|
||||||
|
not: userId,
|
||||||
|
},
|
||||||
|
url,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return await prisma.$transaction(async (tx) => {
|
if (isUrlTaken) {
|
||||||
await tx.userProfile.create({
|
throw new AppError(
|
||||||
data: {
|
AppErrorCode.PROFILE_URL_TAKEN,
|
||||||
profileURL,
|
'Profile URL is taken',
|
||||||
|
'The profile URL is already taken',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
url,
|
||||||
|
userProfile: {
|
||||||
|
upsert: {
|
||||||
|
create: {
|
||||||
|
bio: '',
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
bio: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
await tx.userProfile.update({
|
|
||||||
where: {
|
|
||||||
profileURL: user.profileURL ?? undefined,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
profileURL: profileURL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
@ -43,9 +43,9 @@ model User {
|
|||||||
twoFactorSecret String?
|
twoFactorSecret String?
|
||||||
twoFactorEnabled Boolean @default(false)
|
twoFactorEnabled Boolean @default(false)
|
||||||
twoFactorBackupCodes String?
|
twoFactorBackupCodes String?
|
||||||
profileURL String? @unique
|
url String? @unique
|
||||||
|
|
||||||
UserProfile UserProfile? @relation(fields: [profileURL], references: [profileURL], onDelete: Cascade)
|
userProfile UserProfile?
|
||||||
VerificationToken VerificationToken[]
|
VerificationToken VerificationToken[]
|
||||||
ApiToken ApiToken[]
|
ApiToken ApiToken[]
|
||||||
Template Template[]
|
Template Template[]
|
||||||
@ -56,10 +56,10 @@ model User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model UserProfile {
|
model UserProfile {
|
||||||
profileURL String @id @unique
|
id Int @id
|
||||||
profileBio String?
|
bio String?
|
||||||
|
|
||||||
User User?
|
User User? @relation(fields: [id], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserSecurityAuditLogType {
|
enum UserSecurityAuditLogType {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
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 { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
||||||
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
|
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
|
||||||
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
||||||
@ -80,14 +81,20 @@ export const profileRouter = router({
|
|||||||
.input(ZUpdatePublicProfileMutationSchema)
|
.input(ZUpdatePublicProfileMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const { profileURL } = input;
|
const { url } = input;
|
||||||
|
|
||||||
return await updatePublicProfile({
|
const user = await updatePublicProfile({
|
||||||
id: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
profileURL,
|
url,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { success: true, url: user.url };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
|
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
|
||||||
|
throw AppError.parseErrorToTRPCError(error);
|
||||||
|
}
|
||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export const ZUpdateProfileMutationSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdatePublicProfileMutationSchema = z.object({
|
export const ZUpdatePublicProfileMutationSchema = z.object({
|
||||||
profileURL: z.string().min(1),
|
url: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdatePasswordMutationSchema = z.object({
|
export const ZUpdatePasswordMutationSchema = z.object({
|
||||||
|
|||||||
31
packages/ui/icons/verified.tsx
Normal file
31
packages/ui/icons/verified.tsx
Normal 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';
|
||||||
@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
{
|
{
|
||||||
'ring-2 !ring-red-500 transition-all': props['aria-invalid'],
|
'ring-2 !ring-red-500 transition-all': props['aria-invalid'],
|
||||||
|
|||||||
Reference in New Issue
Block a user