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..a0ed0138f --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/public-profile/page.tsx @@ -0,0 +1,40 @@ +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 { LinkTemplatesForm } from '~/components/forms/link-templates'; +import { PublicProfileForm } from '~/components/forms/public-profile'; + +export const metadata: Metadata = { + title: 'Public profile', +}; + +export default async function PublicProfileSettingsPage() { + const { user } = await getRequiredServerComponentSession(); + + 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 94e366e27..84cc25ac8 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, Webhook } from 'lucide-react'; +import { Braces, CreditCard, Globe2, Lock, User, Users, Webhook } from 'lucide-react'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { cn } from '@documenso/ui/lib/utils'; @@ -101,6 +101,18 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { )} + + + ); }; 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 76cfa80d7..36f43429b 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, Webhook } from 'lucide-react'; +import { Braces, CreditCard, Globe2, Lock, User, Users, Webhook } from 'lucide-react'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { cn } from '@documenso/ui/lib/utils'; @@ -104,6 +104,18 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => { )} + + + ); }; diff --git a/apps/web/src/components/forms/link-templates.tsx b/apps/web/src/components/forms/link-templates.tsx new file mode 100644 index 000000000..2296a1d55 --- /dev/null +++ b/apps/web/src/components/forms/link-templates.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { File } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +export const LinkTemplatesForm = () => { + return ( +
+
+

My templates

+ +

+ Create templates to display in your public profile +

+
+ +
+ +
+
+ ); +}; 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..9f6379cfa --- /dev/null +++ b/apps/web/src/components/forms/public-profile.tsx @@ -0,0 +1,149 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { zodResolver } from '@hookform/resolvers/zod'; +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({ + url: z + .string() + .trim() + .toLowerCase() + .min(1, { message: 'Please enter a valid username.' }) + .regex(/^[a-z0-9-]+$/, { + message: 'Username can only container alphanumeric characters and dashes.', + }), + bio: z + .string() + .trim() + .max(256, { + message: 'Bio cannot be longer than 256 characters.', + }) + .optional(), +}); + +export type TPublicProfileFormSchema = z.infer; + +export type PublicProfileFormProps = { + className?: string; + user: User; +}; + +export const PublicProfileForm = ({ className, user }: PublicProfileFormProps) => { + const router = useRouter(); + + const { toast } = useToast(); + + const form = useForm({ + values: { + url: user.url || '', + }, + resolver: zodResolver(ZPublicProfileFormSchema), + }); + + const watchedBio = form.watch('bio'); + + const { mutateAsync: updatePublicProfile } = trpc.profile.updatePublicProfile.useMutation(); + + const isSubmitting = form.formState.isSubmitting; + + const onFormSubmit = async ({ url, bio }: TPublicProfileFormSchema) => { + try { + await updatePublicProfile({ + url, + bio: bio ?? '', + }); + + 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 sign you In. Please try again later.', + }); + } + } + }; + + return ( +
+ +
+ ( + + Public profile URL + + + + + + + )} + /> + + ( + + Bio + + <> +