diff --git a/apps/marketing/src/components/(marketing)/widget.tsx b/apps/marketing/src/components/(marketing)/widget.tsx index 94fd95bea..88b7f47c9 100644 --- a/apps/marketing/src/components/(marketing)/widget.tsx +++ b/apps/marketing/src/components/(marketing)/widget.tsx @@ -199,7 +199,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => { className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5" onSubmit={handleSubmit(onFormSubmit)} > -

Sign up to Community Plan

+

Sign up to Community Plan

with Timur Ercan & Lucas Smith from Documenso

diff --git a/apps/web/src/app/(dashboard)/settings/public-profile/layout.tsx b/apps/web/src/app/(dashboard)/settings/public-profile/layout.tsx new file mode 100644 index 000000000..bd7755cc4 --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/public-profile/layout.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export type PublicProfileSettingsLayout = { + children: React.ReactNode; +}; + +export default function PublicProfileSettingsLayout({ children }: PublicProfileSettingsLayout) { + return
{children}
; +} diff --git a/apps/web/src/app/(dashboard)/settings/public-profile/page.tsx b/apps/web/src/app/(dashboard)/settings/public-profile/page.tsx new file mode 100644 index 000000000..ddba9386e --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/public-profile/page.tsx @@ -0,0 +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 { 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(); + + return ( + <> + } + className="max-w-xl" + /> + + + + ); +} diff --git a/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx b/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx index 6e183b0c7..9f866ca49 100644 --- a/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx @@ -1,5 +1,8 @@ import type { Metadata } from 'next'; +import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header'; + +import ActivityPageBackButton from '../../../../../components/(dashboard)/settings/layout/activity-back'; import { UserSecurityActivityDataTable } from './user-security-activity-data-table'; export const metadata: Metadata = { @@ -9,11 +12,11 @@ export const metadata: Metadata = { export default function SettingsSecurityActivityPage() { return (
-

Security activity

- -

- View all recent security activity related to your account. -

+ } + />
diff --git a/apps/web/src/app/(unauthenticated)/layout.tsx b/apps/web/src/app/(unauthenticated)/layout.tsx index 43c6d291f..784135c44 100644 --- a/apps/web/src/app/(unauthenticated)/layout.tsx +++ b/apps/web/src/app/(unauthenticated)/layout.tsx @@ -3,6 +3,9 @@ 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 UnauthenticatedLayoutProps = { children: React.ReactNode; @@ -10,18 +13,22 @@ type UnauthenticatedLayoutProps = { export default function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) { return ( -
-
-
- background pattern + <> + +
+
+
+ background pattern +
+ +
{children}
+
- -
{children}
-
-
+ + ); } diff --git a/apps/web/src/app/(unauthenticated)/signin/page.tsx b/apps/web/src/app/(unauthenticated)/signin/page.tsx index 50356a5bb..31baa502f 100644 --- a/apps/web/src/app/(unauthenticated)/signin/page.tsx +++ b/apps/web/src/app/(unauthenticated)/signin/page.tsx @@ -30,36 +30,31 @@ export default function SignInPage({ searchParams }: SignInPageProps) { } return ( -
-

Sign in to your account

+ <> +
+

Sign in to your account

-

- Welcome back, we are lucky to have you. -

- - - - {NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && ( -

- Don't have an account?{' '} - - Sign up - +

+ Welcome back, we are lucky to have you.

- )} -

- - Forgot your password? - -

-
+
+ + + + {process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && ( +

+ Don't have an account?{' '} + + Sign up + +

+ )} +
+ ); } diff --git a/apps/web/src/app/(unauthenticated)/signup-layout.tsx b/apps/web/src/app/(unauthenticated)/signup-layout.tsx new file mode 100644 index 000000000..61c390d13 --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/signup-layout.tsx @@ -0,0 +1,34 @@ +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 SignUpLayoutProps = { + children: React.ReactNode; +}; + +export default function SignUpLayout({ children }: SignUpLayoutProps) { + return ( + <> + +
+
+
+ background pattern +
+ +
{children}
+
+
+
+ + ); +} diff --git a/apps/web/src/app/(unauthenticated)/signup/page.tsx b/apps/web/src/app/(unauthenticated)/signup/page.tsx index b9365e1d5..ed7477041 100644 --- a/apps/web/src/app/(unauthenticated)/signup/page.tsx +++ b/apps/web/src/app/(unauthenticated)/signup/page.tsx @@ -9,6 +9,8 @@ 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', }; @@ -34,26 +36,28 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) { } return ( -
-

Create a new account

+ + <> +

Create a new account

-

- Create your account and start using state-of-the-art document signing. Open and beautiful - signing is within your grasp. -

+

+ Create your account and start using state-of-the-art document signing. Open and beautiful + signing is within your grasp. +

- + -

- Already have an account?{' '} - - Sign in instead - -

-
+

+ Already have an account?{' '} + + Sign in instead + +

+ + ); } diff --git a/apps/web/src/components/(dashboard)/layout/new/new-header.tsx b/apps/web/src/components/(dashboard)/layout/new/new-header.tsx new file mode 100644 index 000000000..66036359c --- /dev/null +++ b/apps/web/src/components/(dashboard)/layout/new/new-header.tsx @@ -0,0 +1,87 @@ +'use client'; + +import type { HTMLAttributes } from 'react'; +import { useState } from 'react'; + +import Image from 'next/image'; +import Link from 'next/link'; + +import LogoImage from '@documenso/assets/logo.png'; +import { cn } from '@documenso/ui/lib/utils'; + +import { NewHamburgerMenu } from './new-mobile-hamburger'; +import { NewMobileNavigation } from './new-mobile-navigation'; + +export type HeaderProps = HTMLAttributes; + +export const NewHeader = ({ className, ...props }: HeaderProps) => { + const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false); + + return ( +
+
+ setIsHamburgerMenuOpen(false)}> + Documenso Logo + +
+ +
+ + Pricing + + + + Blog + + + + Open Startup + + + + Sign in + + + + Sign up + + +
+ + setIsHamburgerMenuOpen((v) => !v)} + isMenuOpen={isHamburgerMenuOpen} + /> + +
+ ); +}; diff --git a/apps/web/src/components/(dashboard)/layout/new/new-mobile-hamburger.tsx b/apps/web/src/components/(dashboard)/layout/new/new-mobile-hamburger.tsx new file mode 100644 index 000000000..8b7666df4 --- /dev/null +++ b/apps/web/src/components/(dashboard)/layout/new/new-mobile-hamburger.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { Menu, X } from 'lucide-react'; + +import { Button } from '@documenso/ui/primitives/button'; + +export interface HamburgerMenuProps { + isMenuOpen: boolean; + onToggleMenuOpen?: () => void; +} + +export const NewHamburgerMenu = ({ isMenuOpen, onToggleMenuOpen }: HamburgerMenuProps) => { + return ( +
+ +
+ ); +}; diff --git a/apps/web/src/components/(dashboard)/layout/new/new-mobile-navigation.tsx b/apps/web/src/components/(dashboard)/layout/new/new-mobile-navigation.tsx new file mode 100644 index 000000000..0d104eeb6 --- /dev/null +++ b/apps/web/src/components/(dashboard)/layout/new/new-mobile-navigation.tsx @@ -0,0 +1,151 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; + +import { motion, useReducedMotion } from 'framer-motion'; +import { FaXTwitter } from 'react-icons/fa6'; +import { LiaDiscord } from 'react-icons/lia'; +import { LuGithub } from 'react-icons/lu'; + +import LogoImage from '@documenso/assets/logo.png'; +import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet'; + +export type MobileNavigationProps = { + isMenuOpen: boolean; + onMenuOpenChange?: (_value: boolean) => void; +}; + +export const MENU_NAVIGATION_LINKS = [ + { + href: 'https://documenso.com/singleplayer', + text: 'Singleplayer', + }, + { + href: 'https://documenso.com/blog', + text: 'Blog', + }, + { + href: 'https://documenso.com/pricing', + text: 'Pricing', + }, + { + href: 'https://documenso.com/open', + text: 'Open Startup', + }, + { + href: 'https://status.documenso.com', + text: 'Status', + }, + { + href: 'mailto:support@documenso.com', + text: 'Support', + target: '_blank', + }, + { + href: 'https://documenso.com/privacy', + text: 'Privacy', + }, + { + href: '/signin', + text: 'Sign in', + }, + { + href: '/signup', + text: 'Sign up', + }, +]; + +export const NewMobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => { + const shouldReduceMotion = useReducedMotion(); + + const handleMenuItemClick = () => { + onMenuOpenChange?.(false); + }; + + return ( + + + + Documenso Logo + + + + {MENU_NAVIGATION_LINKS.map(({ href, text, target }) => ( + + handleMenuItemClick()} + target={target} + > + {href === 'https://app.documenso.com/signup' ? ( + + {text} + + ) : ( + text + )} + + + ))} + + +
+ + + + + + + + + + + +
+
+
+ ); +}; diff --git a/apps/web/src/components/(dashboard)/settings/layout/activity-back.tsx b/apps/web/src/components/(dashboard)/settings/layout/activity-back.tsx new file mode 100644 index 000000000..d6ab1d080 --- /dev/null +++ b/apps/web/src/components/(dashboard)/settings/layout/activity-back.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { Button } from '@documenso/ui/primitives/button'; + +export default function ActivityPageBackButton() { + const router = useRouter(); + return ( +
+ +
+ ); +} diff --git a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx index e87c47b67..17147305f 100644 --- a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx +++ b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx @@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { Braces, CreditCard, Lock, User, Users } from 'lucide-react'; +import { Braces, CreditCard, Globe2, Lock, User, Users } from 'lucide-react'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { cn } from '@documenso/ui/lib/utils'; @@ -91,6 +91,19 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { )} + + + +
); }; diff --git a/apps/web/src/components/(dashboard)/settings/layout/header.tsx b/apps/web/src/components/(dashboard)/settings/layout/header.tsx index 3fe567b81..bc8b33507 100644 --- a/apps/web/src/components/(dashboard)/settings/layout/header.tsx +++ b/apps/web/src/components/(dashboard)/settings/layout/header.tsx @@ -1,21 +1,33 @@ import React from 'react'; +import { cn } from '@documenso/ui/lib/utils'; + export type SettingsHeaderProps = { title: string; subtitle: string; children?: React.ReactNode; + titleChildren?: React.ReactNode; + className?: string; }; -export const SettingsHeader = ({ children, title, subtitle }: SettingsHeaderProps) => { +export const SettingsHeader = ({ + children, + title, + subtitle, + titleChildren, + className, +}: SettingsHeaderProps) => { return ( <> -
+

{title}

{subtitle}

+
{titleChildren}
+ {children}
diff --git a/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx b/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx index ad5ca96f6..152eb59a6 100644 --- a/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx +++ b/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx @@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { Braces, CreditCard, Lock, User, Users } from 'lucide-react'; +import { Braces, CreditCard, Globe2, Lock, User, Users } from 'lucide-react'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { cn } from '@documenso/ui/lib/utils'; @@ -94,6 +94,19 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => { )} + + + +
); }; diff --git a/apps/web/src/components/forms/public-profile.tsx b/apps/web/src/components/forms/public-profile.tsx new file mode 100644 index 000000000..5ceb2c6a5 --- /dev/null +++ b/apps/web/src/components/forms/public-profile.tsx @@ -0,0 +1,165 @@ +'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; + +export type PublicProfileFormProps = { + className?: string; + user: User; +}; + +export const PublicProfileForm = ({ user, className }: PublicProfileFormProps) => { + const textRef = useRef(null); + + const { toast } = useToast(); + const router = useRouter(); + + const form = useForm({ + 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 ( +
+ +
+ ( + + Public profile URL + + <> + + {profileURL && ( + + + {profileURL} + + + + )} + + + + + )} + /> + + ( + + Bio + +