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 (
+
+
+ );
+};
diff --git a/packages/lib/server-only/user/update-public-profile.ts b/packages/lib/server-only/user/update-public-profile.ts
index f70f02cf2..6be54ee49 100644
--- a/packages/lib/server-only/user/update-public-profile.ts
+++ b/packages/lib/server-only/user/update-public-profile.ts
@@ -5,9 +5,10 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
export type UpdatePublicProfileOptions = {
userId: number;
url: string;
+ bio?: string;
};
-export const updatePublicProfile = async ({ userId, url }: UpdatePublicProfileOptions) => {
+export const updatePublicProfile = async ({ userId, url, bio }: UpdatePublicProfileOptions) => {
const isUrlTaken = await prisma.user.findFirst({
select: {
id: true,
@@ -37,10 +38,10 @@ export const updatePublicProfile = async ({ userId, url }: UpdatePublicProfileOp
userProfile: {
upsert: {
create: {
- bio: '',
+ bio: bio,
},
update: {
- bio: '',
+ bio: bio,
},
},
},
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index eb5f54274..d17316148 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -88,7 +88,7 @@ export const profileRouter = router({
.input(ZUpdatePublicProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
- const { url } = input;
+ const { url, bio } = input;
if (IS_BILLING_ENABLED() && url.length <= 6) {
const subscriptions = await getSubscriptionsByUserId({
@@ -108,6 +108,7 @@ export const profileRouter = router({
const user = await updatePublicProfile({
userId: ctx.user.id,
url,
+ bio,
});
return { success: true, url: user.url };
diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index dc62f83ba..541ceede5 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -25,6 +25,13 @@ export const ZUpdatePublicProfileMutationSchema = z.object({
.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 const ZUpdatePasswordMutationSchema = z.object({