Skip to content

Commit

Permalink
Change Username API and component
Browse files Browse the repository at this point in the history
  • Loading branch information
S4nfs committed Aug 5, 2024
1 parent bed0aad commit 418035c
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 1 deletion.
48 changes: 48 additions & 0 deletions Typescript_Course/PROJECTS/Blueeit/src/app/api/username/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { getAuthSession } from '@/lib/auth'
import { db } from '@/lib/db'
import { UsernameValidators } from '@/lib/validators/username'
import { z } from 'zod'

export async function PATCH(req: Request) {
try {
const session = await getAuthSession()

if (!session?.user) {
return new Response('Unauthorized', { status: 401 })
}

const body = await req.json()
const { name } = UsernameValidators.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.user.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 })
}
}
34 changes: 34 additions & 0 deletions Typescript_Course/PROJECTS/Blueeit/src/app/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { redirect } from 'next/navigation'

import { authOptions, getAuthSession } from '@/lib/auth'
import UserNameForm from '@/components/UserNameForm'

export const metadata = {
title: 'Settings',
description: 'Manage account and website settings.',
}

export default async function SettingsPage() {
const session = await getAuthSession()

if (!session?.user) {
redirect(authOptions?.pages?.signIn || '/login')
}

return (
<div className='max-w-4xl mx-auto py-12'>
<div className='grid items-start gap-8'>
<h1 className='font-bold text-3xl md:text-4xl'>Settings</h1>

<div className='grid gap-10'>
<UserNameForm
user={{
id: session.user.id,
username: session.user.username || '',
}}
/>
</div>
</div>
</div>
)
}
95 changes: 95 additions & 0 deletions Typescript_Course/PROJECTS/Blueeit/src/components/UserNameForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client'

import { UsernameRequest, UsernameValidators } from '@/lib/validators/username'
import { zodResolver } from '@hookform/resolvers/zod'
import { User } from '@prisma/client'
import { FC } from 'react'
import { useForm } from 'react-hook-form'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/Card'
import { Label } from './ui/label'
import { Input } from './ui/Input'
import { Button } from './ui/Button'
import { useMutation } from '@tanstack/react-query'
import axios, { AxiosError } from 'axios'
import { toast } from '@/hooks/use-toast'
import { useRouter } from 'next/navigation'

interface UserNameFormProps {
user: Pick<User, 'id' | 'username'>
}

const UserNameForm: FC<UserNameFormProps> = ({ user }) => {
const {
handleSubmit,
register,
formState: { errors },
} = useForm<UsernameRequest>({
resolver: zodResolver(UsernameValidators),
defaultValues: {
name: user?.username || '',
},
})
const router = useRouter()
const { mutate: updateUser, isLoading } = useMutation({
mutationFn: async ({ name }: UsernameRequest) => {
const payload: UsernameRequest = {
name,
}
const { data } = await axios.patch('/api/username', payload)
return data
},
onError: (error) => {
if (error instanceof AxiosError) {
if (error.response?.status === 409) {
//409: conflict with the current state of a resource
return toast({
title: 'Username already taken',
description: 'Please try another name',
variant: 'destructive',
})
}
}
return toast({
title: 'Something went wrong',
description: 'Please try again',
variant: 'destructive',
})
},
onSuccess: () => {
toast({
title: 'Username updated',
description: 'Your username has been updated successfully',
})
router.refresh()
},
})
return (
<form onSubmit={handleSubmit((e) => updateUser(e))}>
<Card>
<CardHeader>
<CardTitle>Your username</CardTitle>
<CardDescription>Please enter a username to display</CardDescription>
</CardHeader>
<CardContent>
<div className='relatve grid gap-'>
<div className='absolute top-0 left-0 w-8 h-8 grid place-items-center'>
<span className='text-sm text-zinc-400'>u/</span>
</div>
<Label className='sr-only' htmlFor='name'>
Name
</Label>
<Input id='name' className='w-[400px] pl-600' size={32} {...register('name')} />
{errors?.name && <span className='text-red-400 text-sm'>{errors.name.message}</span>}
</div>
</CardContent>
<CardFooter>
<Button type='submit' isLoading={isLoading}>
Change Name
</Button>
</CardFooter>
</Card>
</form>
)
}

export default UserNameForm
76 changes: 76 additions & 0 deletions Typescript_Course/PROJECTS/Blueeit/src/components/ui/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as React from "react"

import { cn } from "@/lib/utils"

const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"

const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"

const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"

const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"

const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"

const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const UserAccountNav: FC<UserAccountNavProps> = ({ user }) => {
<div className='flex items-center justify-start gap-2 p-2'>
<div className='flex flex-col space-y-1 leading-none'>
{user.name && <p className='font-medium'>{user.name}</p>}
{user.email && <p className='w-[200px] truncate text-sm text-zinc-700'>{user.email}</p>}
{user.email && <p className='w-[200px] truncate text-sm text-muted-foreground'>{user.email}</p>}
</div>
</div>
<DropdownMenuSeparator />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { z } from "zod";

export const UsernameValidators = z.object({
name: z.string().min(3).max(32).regex(/^[a-zA-Z0-9_]+$/),
})
export type UsernameRequest = z.infer<typeof UsernameValidators>;

0 comments on commit 418035c

Please sign in to comment.