mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 02:32:00 +10:00
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
import backgroundPattern from '@documenso/assets/images/background-lw-2.png';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { AnnouncementBar } from '@documenso/ui/primitives/announcement-bar';
|
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,
|
'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" />
|
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
export type DocumentsPageProps = {
|
export type DocumentsPageProps = {
|
||||||
searchParams?: DocumentsPageViewProps['searchParams'];
|
searchParams?: DocumentsPageViewProps['searchParams'];
|
||||||
@ -11,6 +14,12 @@ export const metadata: Metadata = {
|
|||||||
title: 'Documents',
|
title: 'Documents',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||||
return <DocumentsPageView searchParams={searchParams} />;
|
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 * as React from 'react';
|
||||||
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
|
||||||
import { Switch } from '@documenso/ui/primitives/switch';
|
|
||||||
|
|
||||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||||
import { PublicProfileForm } from '~/components/forms/public-profile';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Public Profile',
|
title: 'Public Profile',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function PublicProfilePage() {
|
export default function PublicProfilePage() {
|
||||||
const { user } = await getRequiredServerComponentSession();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
title="Public profile"
|
title="Public profile"
|
||||||
subtitle="You can choose to enable/disable your profile for public view"
|
subtitle=""
|
||||||
titleChildren={<Switch></Switch>}
|
|
||||||
className="max-w-xl"
|
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
|
<Image
|
||||||
src={backgroundPattern}
|
src={backgroundPattern}
|
||||||
alt="background pattern"
|
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>
|
||||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -7,8 +7,6 @@ import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt'
|
|||||||
|
|
||||||
import { SignUpForm } from '~/components/forms/signup';
|
import { SignUpForm } from '~/components/forms/signup';
|
||||||
|
|
||||||
import SignUpLayout from '../signup-layout';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Sign Up',
|
title: 'Sign Up',
|
||||||
};
|
};
|
||||||
@ -32,7 +30,6 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SignUpLayout>
|
|
||||||
<>
|
<>
|
||||||
<h1 className="text-3xl font-semibold">Create a new account</h1>
|
<h1 className="text-3xl font-semibold">Create a new account</h1>
|
||||||
|
|
||||||
@ -54,6 +51,5 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
</SignUpLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -79,7 +79,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link href="/settings/public-profile">
|
<Link href="#">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -89,6 +89,12 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
|||||||
>
|
>
|
||||||
<Globe2 className="mr-2 h-5 w-5" />
|
<Globe2 className="mr-2 h-5 w-5" />
|
||||||
Public profile
|
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>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -82,7 +82,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link href="/settings/public-profile">
|
<Link href="#">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -92,6 +92,12 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
|||||||
>
|
>
|
||||||
<Globe2 className="mr-2 h-5 w-5" />
|
<Globe2 className="mr-2 h-5 w-5" />
|
||||||
Public profile
|
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>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</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,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@ -55,6 +57,12 @@ export type SignUpFormProps = {
|
|||||||
export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: SignUpFormProps) => {
|
export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: SignUpFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
let src: string | null = null;
|
||||||
|
if (searchParams) {
|
||||||
|
src = searchParams.get('src');
|
||||||
|
}
|
||||||
|
|
||||||
const form = useForm<TSignUpFormSchema>({
|
const form = useForm<TSignUpFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
@ -83,6 +91,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
|||||||
analytics.capture('App: User Sign Up', {
|
analytics.capture('App: User Sign Up', {
|
||||||
email,
|
email,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
custom_campaign_params: { src },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||||
@ -127,7 +136,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
|||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Full Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="text" {...field} />
|
<Input type="text" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -141,7 +150,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
|||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Email Address</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="email" {...field} />
|
<Input type="email" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export function PostHogPageview() {
|
|||||||
// Do nothing.
|
// 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 = {
|
export type UpdatePublicProfileOptions = {
|
||||||
id: User['id'];
|
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 });
|
const user = await getUserById({ id });
|
||||||
console.log('Adi', user);
|
|
||||||
// Existence check
|
// Existence check
|
||||||
await prisma.userProfile.findFirstOrThrow({
|
await prisma.userProfile.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
profileURL: user.profileURL ?? '',
|
profileURL: user.profileURL ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return await prisma.$transaction(async (tx) => {
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.userProfile.create({
|
||||||
|
data: {
|
||||||
|
profileURL,
|
||||||
|
},
|
||||||
|
});
|
||||||
await tx.userProfile.update({
|
await tx.userProfile.update({
|
||||||
where: {
|
where: {
|
||||||
profileURL: user.profileURL!,
|
profileURL: user.profileURL ?? undefined,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
profileURL: userProfile.profileURL,
|
profileURL: profileURL,
|
||||||
profileBio: userProfile.profileBio,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -79,11 +79,11 @@ export const profileRouter = router({
|
|||||||
.input(ZUpdatePublicProfileMutationSchema)
|
.input(ZUpdatePublicProfileMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const { profileURL, profileBio } = input;
|
const { profileURL } = input;
|
||||||
|
|
||||||
return await updatePublicProfile({
|
return await updatePublicProfile({
|
||||||
id: ctx.user.id,
|
id: ctx.user.id,
|
||||||
userProfile: { profileURL, profileBio },
|
profileURL,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@ -18,7 +18,6 @@ export const ZUpdateProfileMutationSchema = z.object({
|
|||||||
|
|
||||||
export const ZUpdatePublicProfileMutationSchema = z.object({
|
export const ZUpdatePublicProfileMutationSchema = z.object({
|
||||||
profileURL: z.string().min(1),
|
profileURL: z.string().min(1),
|
||||||
profileBio: z.string().max(256, { message: 'Profile bio must not exceed 256 characters' }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdatePasswordMutationSchema = z.object({
|
export const ZUpdatePasswordMutationSchema = z.object({
|
||||||
|
|||||||
@ -1,18 +1,24 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
interface AnnouncementBarProps {
|
interface AnnouncementBarProps {
|
||||||
isShown: boolean;
|
isShown: boolean;
|
||||||
|
className: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AnnouncementBar: React.FC<AnnouncementBarProps> = ({ isShown }) => {
|
export const AnnouncementBar: React.FC<AnnouncementBarProps> = ({ isShown, className }) => {
|
||||||
return (
|
return (
|
||||||
isShown && (
|
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">
|
<div className="text-center">
|
||||||
<span className="text-sm text-gray-800">
|
<span className="text-sm text-white">Claim your documenso public profile URL now!</span>{' '}
|
||||||
Claim your documenso public profile URL now!
|
<span className="text-sm font-medium text-white">documenso.com/u/yourname</span>
|
||||||
</span>{' '}
|
|
||||||
<span className="text-sm font-medium text-gray-800">documenso.com/u/yourname</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-4 rounded-lg bg-white px-3 py-1">
|
<div className="flex items-center justify-center gap-4 rounded-lg bg-white px-3 py-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user