diff --git a/app/(content)/(user)/help/page.tsx b/app/(content)/(user)/help/page.tsx index 20507e4d951c8e0024eeec737169df1786312ac3..b7ccf79aba0bcebd5d6bae02bed5931768ba0ae1 100644 --- a/app/(content)/(user)/help/page.tsx +++ b/app/(content)/(user)/help/page.tsx @@ -5,7 +5,7 @@ export default function HelpPage() { return ( <GlobalLayout mainContent={ - <Card> + <Card className="w-full overflow-hidden p-0 md:p-6"> <CardHeader> <CardTitle> Help / FAQ @@ -18,45 +18,39 @@ export default function HelpPage() { Fill in the required information and follow the prompts to complete the registration process. </p> - <p className="font-bold pt-6">2. How can I reset my password?</p> - <p> - If you forgot your password, go to the login page and click on the "Forgot Password" link. - Enter your email address and follow the instructions sent to your email to reset your password. - </p> - - <p className="font-bold pt-6">3. How can I create a new post?</p> + <p className="font-bold pt-6">2. How can I create a new post?</p> <p> To create a new post, you have multiple options depending on where you are on the website. - If you're on the homepage, community page, or your profile page, you'll find a text field where you can write your post. + If you're on the homepage, or your profile page, you'll find a text field where you can write your post. Once you're done, simply click the "Post" button to publish your content and share it with others. </p> - <p className="font-bold pt-6">4. How can I edit or delete my post?</p> + <p className="font-bold pt-6">3. How can I delete my post?</p> <p> - To edit or delete your post, go to the Home page or your profile page and locate your post. - Depending on the platform, there may be options to edit or delete the post. + To delete your post, go to the Home page or your profile page and locate your post. + Depending on the platform, there may be options to delete the post. Click on the respective option and follow the prompts to make the desired changes. </p> - <p className="font-bold pt-6">5. How do I follow a community or user?</p> + <p className="font-bold pt-6">4. How do I follow a user?</p> <p> - To follow a community or user, visit their profile or community page and look for the "Follow" button. + To follow a user, visit their profile page and look for the "Follow" button. Click on it, and you'll start receiving updates and notifications from them. </p> - <p className="font-bold pt-6">6. How do I customize my profile?</p> + <p className="font-bold pt-6">5. How do I customize my profile?</p> <p> To customize your profile, go to the settings of your account. There, you can upload a profile picture, add a bio, update your personal information, and adjust privacy settings according to your preferences. </p> - <p className="font-bold pt-6">7. Can I change my username?</p> + <p className="font-bold pt-6">6. Can I change my username?</p> <p> In most cases, you can change your username by going to the account settings. Look for the option to edit your name and follow the provided instructions to make the change. </p> - <p className="font-bold pt-6"> 8. How do I delete my account?</p> + <p className="font-bold pt-6">7. How do I delete my account?</p> <p> If you wish to delete your account, go to the account settings “Account Settings†section and look for the option to delete your account. Follow the provided steps and confirm your decision to permanently delete your account. Please note that this action is irreversible and will result in the loss of all associated data. diff --git a/app/(content)/(user)/settings/page.tsx b/app/(content)/(user)/settings/page.tsx index b72aec206f16aa905a986d1a1c0ecfa136052b44..e4c048eec25cc2850ad9fd7fc9d32c64762ecbf7 100644 --- a/app/(content)/(user)/settings/page.tsx +++ b/app/(content)/(user)/settings/page.tsx @@ -1,7 +1,36 @@ -export default function Settings() { +import { redirect } from 'next/navigation' + +import { GlobalLayout } from '@/components/global-layout' +import { Card } from '@/components/ui/card' +import { UserNameForm } from '@/components/user-name-form' +import { authOptions } from '@/lib/auth' +import { getCurrentUser } from '@/lib/session' + +export const metadata = { + title: 'Settings', + description: 'Manage account and website settings.', +} + +export default async function SettingsPage() { + const session = await getCurrentUser() + + if (!session) { + redirect(authOptions?.pages?.signIn || '/login') + } + return ( - <div> - <h1>Settings Page WIP</h1> - </div> + <GlobalLayout + mainContent={ + <Card className="w-full overflow-hidden p-6 md:p-12 space-y-6"> + <h1 className="text-2xl font-semibold leading-none tracking-tight">Settings</h1> + <UserNameForm + user={{ + id: session.id, + username: session.username || '', + }} + /> + </Card> + } + /> ) } \ No newline at end of file diff --git a/app/api/username/route.ts b/app/api/username/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..396daa04c97f17b3ddd3037501809f47c42a1dfc --- /dev/null +++ b/app/api/username/route.ts @@ -0,0 +1,51 @@ +import { db } from '@/lib/db' +import { getCurrentUser } from '@/lib/session' +import { UsernameValidator } from '@/lib/validations/username' +import { z } from 'zod' + +export async function PATCH(req: Request) { + try { + const session = await getCurrentUser() + + if (!session) { + return new Response('Unauthorized', { status: 401 }) + } + + const body = await req.json() + const { name } = UsernameValidator.parse(body) + + // check if username is taken + const username = await db.user.findFirst({ + where: { + username: name, + }, + }) + + if (username) { + return new Response('Username is taken', { status: 409 }) + } + + // update username + await db.user.update({ + where: { + id: session.id, + }, + data: { + username: name, + }, + }) + + return new Response('OK') + } catch (error) { + (error) + + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 400 }) + } + + return new Response( + 'Could not update username at this time. Please try later', + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/components/nav-mobile.tsx b/components/nav-mobile.tsx index 2196f5bdc9681c9666152f85cee1ffdbdb3fa85a..3d24e889f59170e57073e342c621db4790893903 100644 --- a/components/nav-mobile.tsx +++ b/components/nav-mobile.tsx @@ -26,7 +26,7 @@ export const MobileNav = ({ items, user }: { items: SidebarNavItem[], user: User if (item.title === "Settings") { return } - if (item.title === "Help") { + if (item.title === "Help / FAQ") { return } return ( diff --git a/components/user-name-form.tsx b/components/user-name-form.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a9a6bb60d6b8afef336767bc7a59b30c93714583 --- /dev/null +++ b/components/user-name-form.tsx @@ -0,0 +1,114 @@ +'use client' + +import { zodResolver } from '@hookform/resolvers/zod' +import { User } from '@prisma/client' +import { useRouter } from 'next/navigation' +import * as React from 'react' +import { useForm } from 'react-hook-form' +import * as z from 'zod' + +import { cn } from '@/lib/utils' +import { UsernameValidator } from '@/lib/validations/username' +import { useMutation } from '@tanstack/react-query' +import axios, { AxiosError } from 'axios' +import { Icons } from './icons' +import { Button } from './ui/button' +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card' +import { Input } from './ui/input' +import { Label } from './ui/label' +import { toast } from './ui/use-toast' + +interface UserNameFormProps extends React.HTMLAttributes<HTMLFormElement> { + user: Pick<User, 'id' | 'username'> +} + +type FormData = z.infer<typeof UsernameValidator> + +export function UserNameForm({ user, className, ...props }: UserNameFormProps) { + const router = useRouter() + const { + handleSubmit, + register, + formState: { errors }, + } = useForm<FormData>({ + resolver: zodResolver(UsernameValidator), + defaultValues: { + name: user?.username || '', + }, + }) + + const { mutate: updateUsername, isLoading } = useMutation({ + mutationFn: async ({ name }: FormData) => { + const payload: FormData = { name } + + const { data } = await axios.patch(`/api/username/`, payload) + return data + }, + onError: (err) => { + if (err instanceof AxiosError) { + if (err.response?.status === 409) { + return toast({ + title: 'Username already taken.', + description: 'Please choose another username.', + variant: 'destructive', + }) + } + } + + return toast({ + title: 'Something went wrong.', + description: 'Your username was not updated. Please try again.', + variant: 'destructive', + }) + }, + onSuccess: () => { + toast({ + description: 'Your username has been updated.', + }) + router.refresh() + }, + }) + + return ( + <form + className={cn(className)} + onSubmit={handleSubmit((e) => updateUsername(e))} + {...props}> + <Card> + <CardHeader> + <CardTitle>Your username</CardTitle> + <CardDescription> + Please enter a display name you are comfortable with. + </CardDescription> + </CardHeader> + <CardContent> + <div className='relative grid gap-1'> + <div className='absolute top-0 left-0 w-8 h-10 grid place-items-center'> + <span className='text-sm text-zinc-400'>@</span> + </div> + <Label className='sr-only' htmlFor='name'> + Name + </Label> + <Input + id='name' + className='w-[400px] pl-6' + size={32} + {...register('name')} + /> + {errors?.name && ( + <p className='px-1 text-xs text-red-600'>{errors.name.message}</p> + )} + </div> + </CardContent> + <CardFooter> + <Button disabled={isLoading} type="submit"> + {isLoading && ( + <Icons.spinner className="mr-2 h-4 w-4 animate-spin" /> + )} + Change name + </Button> + </CardFooter> + </Card> + </form> + ) +} \ No newline at end of file diff --git a/lib/config/dashboard.ts b/lib/config/dashboard.ts index 8f3fda31561e902c4f26c3edc10340460d1b9f3b..bdc91b9e5cff013c1825f4f37840658a3bbf836f 100644 --- a/lib/config/dashboard.ts +++ b/lib/config/dashboard.ts @@ -32,11 +32,11 @@ export const dashboardConfig: DashboardConfig = { href: "", icon: "user", }, - // { - // title: "Settings", - // href: "/settings", - // icon: "settings", - // }, + { + title: "Settings", + href: "/settings", + icon: "settings", + }, { title: "Help / FAQ", href: "/help", diff --git a/lib/validations/username.ts b/lib/validations/username.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d5202d5af77e1e722eea12ae2afd564a339b97b --- /dev/null +++ b/lib/validations/username.ts @@ -0,0 +1,9 @@ +import { z } from 'zod' + +export const UsernameValidator = z.object({ + name: z + .string() + .min(3) + .max(32) + .regex(/^[a-zA-Z0-9_]+$/), +}) \ No newline at end of file