-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
260 additions
and
1 deletion.
There are no files selected for viewing
48 changes: 48 additions & 0 deletions
48
Typescript_Course/PROJECTS/Blueeit/src/app/api/username/route.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
34
Typescript_Course/PROJECTS/Blueeit/src/app/settings/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
95
Typescript_Course/PROJECTS/Blueeit/src/components/UserNameForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
76
Typescript_Course/PROJECTS/Blueeit/src/components/ui/Card.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
Typescript_Course/PROJECTS/Blueeit/src/lib/validators/username.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |