mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
@ -2,8 +2,10 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-lw-2.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { AnnouncementBar } from '@documenso/ui/primitives/announcement-bar';
|
||||
|
||||
@ -39,7 +41,14 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
||||
'bg-background/50 backdrop-blur-md': scrollY > 5,
|
||||
})}
|
||||
>
|
||||
<AnnouncementBar isShown={true} />
|
||||
<div className="absolute -inset-0 -z-[1] opacity-100">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="!h-34 w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<AnnouncementBar className="relative" isShown={true} />
|
||||
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
|
||||
import type { DocumentsPageViewProps } from './documents-page-view';
|
||||
import { DocumentsPageView } from './documents-page-view';
|
||||
import { PublicProfileIntro } from './username-claim/public-profile-intro';
|
||||
|
||||
export type DocumentsPageProps = {
|
||||
searchParams?: DocumentsPageViewProps['searchParams'];
|
||||
@ -11,6 +14,12 @@ export const metadata: Metadata = {
|
||||
title: 'Documents',
|
||||
};
|
||||
|
||||
export default function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||
return <DocumentsPageView searchParams={searchParams} />;
|
||||
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
return (
|
||||
<>
|
||||
<PublicProfileIntro user={user} />
|
||||
<DocumentsPageView searchParams={searchParams} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,225 @@
|
||||
'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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,30 +1,30 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { Switch } from '@documenso/ui/primitives/switch';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||
import { PublicProfileForm } from '~/components/forms/public-profile';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Public Profile',
|
||||
};
|
||||
|
||||
export default async function PublicProfilePage() {
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
export default function PublicProfilePage() {
|
||||
return (
|
||||
<>
|
||||
<SettingsHeader
|
||||
title="Public profile"
|
||||
subtitle="You can choose to enable/disable your profile for public view"
|
||||
titleChildren={<Switch></Switch>}
|
||||
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>
|
||||
}
|
||||
/>
|
||||
|
||||
<PublicProfileForm user={user} className="max-w-xl" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export default function SignInLayout({ children }: SignInLayoutProps) {
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="flex flex-col overflow-hidden dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
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">
|
||||
|
||||
26
apps/web/src/app/(unauthenticated)/signup/layout.tsx
Normal file
26
apps/web/src/app/(unauthenticated)/signup/layout.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
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-6 py-16 md:p-10 lg:p-[7.0rem]">
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -9,8 +9,6 @@ import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt'
|
||||
|
||||
import { SignUpForm } from '~/components/forms/signup';
|
||||
|
||||
import SignUpLayout from '../signup-layout';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Sign Up',
|
||||
};
|
||||
@ -36,28 +34,26 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<SignUpLayout>
|
||||
<>
|
||||
<h1 className="text-3xl font-semibold">Create a new account</h1>
|
||||
<>
|
||||
<h1 className="text-3xl font-semibold">Create a new account</h1>
|
||||
|
||||
<p className="text-muted-foreground/60 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>
|
||||
<p className="text-muted-foreground/60 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>
|
||||
|
||||
<SignUpForm
|
||||
className="mt-1"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
/>
|
||||
<SignUpForm
|
||||
className="mt-1"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
/>
|
||||
|
||||
<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>
|
||||
</>
|
||||
</SignUpLayout>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { BadgeCheck, File } from 'lucide-react';
|
||||
|
||||
import Timur from '@documenso/assets/images/Timur.png';
|
||||
import backgroundPattern from '@documenso/assets/images/background-blog-og.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardFooter, CardHeader } from '@documenso/ui/primitives/card';
|
||||
|
||||
export default function ClaimUsernameCard() {
|
||||
const onSignUpClick = () => {};
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center bg-contain opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[100%] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
<Card className={cn('relative px-16 py-16')}>
|
||||
<Card className="flex flex-col items-center px-6 py-6">
|
||||
<code className="bg-muted rounded-md px-1 py-1 text-sm">
|
||||
<span>documenso.com/u/timur</span>
|
||||
</code>
|
||||
<Avatar className="dark:border-border mt-2 h-12 w-12 border-2 border-solid border-white">
|
||||
<AvatarImage className="AvatarImage" src={Timur.src} alt="Timur" />
|
||||
<AvatarFallback className="text-xs text-gray-400">Timur</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-row gap-x-2">
|
||||
Timur Ercan <BadgeCheck fill="#A2E771" />
|
||||
</div>
|
||||
<span className="text-center">
|
||||
Hey I’m Timur <br /> Pick any of the following agreements below and start signing to get
|
||||
started
|
||||
</span>
|
||||
</Card>
|
||||
<Card className="mt-2 items-center">
|
||||
<CardHeader className="p-2">Documents</CardHeader>
|
||||
<hr className="mb-2" />
|
||||
<div className="mb-2 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<File className="ml-3" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-md">NDA.pdf</span>
|
||||
<span className="text-muted-foregroun mt-0.5 text-xs">
|
||||
Like to discuss about my work?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mr-3" variant="default">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
<hr className="mb-2" />
|
||||
<div className="mb-2 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<File className="ml-3" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-md">NDA.pdf</span>
|
||||
<span className="text-muted-foregroun mt-0.5 text-xs">
|
||||
Like to discuss about my work?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mr-3" variant="default">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<CardFooter className="mt-20 justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Claim Community Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
-80%
|
||||
</span>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -92,7 +92,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link href="/settings/public-profile">
|
||||
<Link href="#">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
@ -102,6 +102,12 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
>
|
||||
<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>
|
||||
|
||||
@ -95,7 +95,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link href="/settings/public-profile">
|
||||
<Link href="#">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
@ -105,6 +105,12 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
>
|
||||
<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>
|
||||
|
||||
@ -1,165 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Copy } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { TRPCClientError } from '@documenso/trpc/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export const ZPublicProfileFormSchema = z.object({
|
||||
profileURL: z.string().trim().min(1, { message: 'Please enter a valid URL slug.' }),
|
||||
profileBio: z
|
||||
.string()
|
||||
.max(256, { message: 'Profile bio must not exceed 256 characters' })
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TPublicProfileFormSchema = z.infer<typeof ZPublicProfileFormSchema>;
|
||||
|
||||
export type PublicProfileFormProps = {
|
||||
className?: string;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const PublicProfileForm = ({ user, className }: PublicProfileFormProps) => {
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<TPublicProfileFormSchema>({
|
||||
values: {
|
||||
profileURL: user.profileURL || '',
|
||||
},
|
||||
resolver: zodResolver(ZPublicProfileFormSchema),
|
||||
});
|
||||
|
||||
const isSaving = form.formState.isSubmitting;
|
||||
|
||||
const { mutateAsync: updatePublicProfile, data: profileURL } =
|
||||
trpc.profile.updatePublicProfile.useMutation();
|
||||
|
||||
const copyTextToClipboard = async () => {
|
||||
if (textRef.current) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(textRef.current.textContent || '');
|
||||
} catch (err) {
|
||||
console.log('Failed to copy: ', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onFormSubmit = async ({ profileURL, profileBio }: TPublicProfileFormSchema) => {
|
||||
try {
|
||||
await updatePublicProfile({
|
||||
profileURL,
|
||||
profileBio: profileBio || '',
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Public profile updated',
|
||||
description: 'Your public profile has been updated successfully.',
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
title: 'An error occurred',
|
||||
description: err.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'An unknown error occurred',
|
||||
variant: 'destructive',
|
||||
description:
|
||||
'We encountered an unknown error while attempting to save your details. Please try again later.',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={(cn('flex w-full flex-col gap-y-4'), className)}
|
||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSaving}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profileURL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Public profile URL</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
<Input type="text" className="mb-2 mt-2" {...field} />
|
||||
{profileURL && (
|
||||
<code className="bg-muted rounded-md px-1 py-1 text-sm">
|
||||
<span ref={textRef} id="textToCopy">
|
||||
{profileURL}
|
||||
</span>
|
||||
<Button
|
||||
className="pointer-events-none w-12 px-0"
|
||||
onClick={copyTextToClipboard}
|
||||
variant="ghost"
|
||||
asChild
|
||||
>
|
||||
<Copy className="h-8 w-8" />
|
||||
</Button>
|
||||
</code>
|
||||
)}
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profileBio"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bio</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea className="mt-2" placeholder="Write about yourself..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<div className="ml-auto mt-4">
|
||||
<Button type="submit" loading={isSaving}>
|
||||
{isSaving ? 'Saving changes...' : 'Save changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { signIn } from 'next-auth/react';
|
||||
@ -58,6 +59,9 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
const { toast } = useToast();
|
||||
const analytics = useAnalytics();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const src = searchParams?.get('src') ?? null;
|
||||
|
||||
const form = useForm<TSignUpFormSchema>({
|
||||
values: {
|
||||
@ -89,6 +93,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
analytics.capture('App: User Sign Up', {
|
||||
email,
|
||||
timestamp: new Date().toISOString(),
|
||||
custom_campaign_params: { src },
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
@ -133,7 +138,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>Full Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
@ -147,7 +152,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>Email Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
|
||||
@ -32,6 +32,7 @@ export function PostHogPageview() {
|
||||
// Do nothing.
|
||||
});
|
||||
},
|
||||
custom_campaign_params: ['src'],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
BIN
packages/assets/images/Lucas.png
Normal file
BIN
packages/assets/images/Lucas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
packages/assets/images/Timur.png
Normal file
BIN
packages/assets/images/Timur.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
BIN
packages/assets/images/background-lw-2.png
Normal file
BIN
packages/assets/images/background-lw-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@ -1,14 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { UserProfile } from '@documenso/prisma/client';
|
||||
|
||||
export interface GetPublicProfileByURLOptions {
|
||||
profileURL: UserProfile['profileURL'];
|
||||
}
|
||||
|
||||
export const getPublicProfileByURL = async ({ profileURL }: GetPublicProfileByURLOptions) => {
|
||||
return await prisma.userProfile.findFirstOrThrow({
|
||||
where: {
|
||||
profileURL: profileURL,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -5,27 +5,30 @@ import { getUserById } from './get-user-by-id';
|
||||
|
||||
export type UpdatePublicProfileOptions = {
|
||||
id: User['id'];
|
||||
userProfile: UserProfile;
|
||||
profileURL: UserProfile['profileURL'];
|
||||
};
|
||||
|
||||
export const updatePublicProfile = async ({ id, userProfile }: UpdatePublicProfileOptions) => {
|
||||
export const updatePublicProfile = async ({ id, profileURL }: UpdatePublicProfileOptions) => {
|
||||
const user = await getUserById({ id });
|
||||
console.log('Adi', user);
|
||||
// Existence check
|
||||
await prisma.userProfile.findFirstOrThrow({
|
||||
where: {
|
||||
profileURL: user.profileURL ?? '',
|
||||
profileURL: user.profileURL ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await tx.userProfile.create({
|
||||
data: {
|
||||
profileURL,
|
||||
},
|
||||
});
|
||||
await tx.userProfile.update({
|
||||
where: {
|
||||
profileURL: user.profileURL!,
|
||||
profileURL: user.profileURL ?? undefined,
|
||||
},
|
||||
data: {
|
||||
profileURL: userProfile.profileURL,
|
||||
profileBio: userProfile.profileBio,
|
||||
profileURL: profileURL,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -80,11 +80,11 @@ export const profileRouter = router({
|
||||
.input(ZUpdatePublicProfileMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { profileURL, profileBio } = input;
|
||||
const { profileURL } = input;
|
||||
|
||||
return await updatePublicProfile({
|
||||
id: ctx.user.id,
|
||||
userProfile: { profileURL, profileBio },
|
||||
profileURL,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -18,7 +18,6 @@ export const ZUpdateProfileMutationSchema = z.object({
|
||||
|
||||
export const ZUpdatePublicProfileMutationSchema = z.object({
|
||||
profileURL: z.string().min(1),
|
||||
profileBio: z.string().max(256, { message: 'Profile bio must not exceed 256 characters' }),
|
||||
});
|
||||
|
||||
export const ZUpdatePasswordMutationSchema = z.object({
|
||||
|
||||
@ -1,18 +1,24 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
interface AnnouncementBarProps {
|
||||
isShown: boolean;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const AnnouncementBar: React.FC<AnnouncementBarProps> = ({ isShown }) => {
|
||||
export const AnnouncementBar: React.FC<AnnouncementBarProps> = ({ isShown, className }) => {
|
||||
return (
|
||||
isShown && (
|
||||
<div className="flex h-full w-full items-center justify-center gap-4 border-b-2 bg-green-400 p-1">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full w-full items-center justify-center gap-4 border-b-2 p-1',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="text-center">
|
||||
<span className="text-sm text-gray-800">
|
||||
Claim your documenso public profile URL now!
|
||||
</span>{' '}
|
||||
<span className="text-sm font-medium text-gray-800">documenso.com/u/yourname</span>
|
||||
<span className="text-sm text-white">Claim your documenso public profile URL now!</span>{' '}
|
||||
<span className="text-sm font-medium text-white">documenso.com/u/yourname</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-4 rounded-lg bg-white px-3 py-1">
|
||||
|
||||
Reference in New Issue
Block a user