mirror of
https://github.com/ershisan99/instagram-client-front.git
synced 2025-12-16 12:33:26 +00:00
lesson 4
This commit is contained in:
@@ -9,12 +9,14 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formkit/tempo": "^0.1.2",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@it-incubator/prettier-config": "^0.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@reduxjs/toolkit": "^2.2.6",
|
||||
"@types/javascript-time-ago": "^2.0.8",
|
||||
"async-mutex": "^0.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"javascript-time-ago": "^2.5.10",
|
||||
"next": "14.2.5",
|
||||
@@ -25,6 +27,7 @@
|
||||
"react-hook-form": "7.52.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-time-ago": "^7.3.3",
|
||||
"sonner": "^1.5.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@formkit/tempo':
|
||||
specifier: ^0.1.2
|
||||
version: 0.1.2
|
||||
'@hookform/resolvers':
|
||||
specifier: ^3.9.0
|
||||
version: 3.9.0(react-hook-form@7.52.1(react@18.3.1))
|
||||
@@ -26,6 +29,9 @@ importers:
|
||||
'@types/javascript-time-ago':
|
||||
specifier: ^2.0.8
|
||||
version: 2.0.8
|
||||
async-mutex:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
@@ -56,6 +62,9 @@ importers:
|
||||
react-time-ago:
|
||||
specifier: ^7.3.3
|
||||
version: 7.3.3(javascript-time-ago@2.5.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
sonner:
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.23.8
|
||||
@@ -109,6 +118,9 @@ packages:
|
||||
resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@formkit/tempo@0.1.2':
|
||||
resolution: {integrity: sha512-jNPPbjL8oj7hK3eHX++CwbR6X4GKQt+x00/q4yeXkwynXHGKL27dylYhpEgwrmediPP4y7s0XtN1if/M/JYujg==}
|
||||
|
||||
'@hookform/resolvers@3.9.0':
|
||||
resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==}
|
||||
peerDependencies:
|
||||
@@ -509,6 +521,9 @@ packages:
|
||||
ast-types-flow@0.0.8:
|
||||
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
||||
|
||||
async-mutex@0.5.0:
|
||||
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1577,6 +1592,12 @@ packages:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
sonner@1.5.0:
|
||||
resolution: {integrity: sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
|
||||
source-map-js@1.2.0:
|
||||
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1815,6 +1836,8 @@ snapshots:
|
||||
|
||||
'@eslint/js@8.57.0': {}
|
||||
|
||||
'@formkit/tempo@0.1.2': {}
|
||||
|
||||
'@hookform/resolvers@3.9.0(react-hook-form@7.52.1(react@18.3.1))':
|
||||
dependencies:
|
||||
react-hook-form: 7.52.1(react@18.3.1)
|
||||
@@ -2197,6 +2220,10 @@ snapshots:
|
||||
|
||||
ast-types-flow@0.0.8: {}
|
||||
|
||||
async-mutex@0.5.0:
|
||||
dependencies:
|
||||
tslib: 2.6.3
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.0.0
|
||||
@@ -3438,6 +3465,11 @@ snapshots:
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
sonner@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
source-map-js@1.2.0: {}
|
||||
|
||||
stop-iteration-iterator@1.0.0:
|
||||
|
||||
33
src/components/textarea.tsx
Normal file
33
src/components/textarea.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ComponentPropsWithoutRef, ElementRef, forwardRef, useId } from 'react'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
export type TextareaProps = ComponentPropsWithoutRef<'textarea'> & {
|
||||
errorMessage?: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<ElementRef<'textarea'>, TextareaProps>(
|
||||
({ className, label, errorMessage, id, ...props }, ref) => {
|
||||
const generatedId = useId()
|
||||
const finalId = id ?? generatedId
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col gap-0.5'}>
|
||||
{!!label && (
|
||||
<label className={'text-sm text-slate-700'} htmlFor={finalId}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
className={clsx('border rounded-md p-3 resize-none', className)}
|
||||
id={finalId}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
{!!errorMessage && <p className={'text-sm text-red-500'}>{errorMessage}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Textarea.displayName = 'Textarea'
|
||||
27
src/form/form-textarea.tsx
Normal file
27
src/form/form-textarea.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Control, FieldValues, useController, UseControllerProps } from 'react-hook-form'
|
||||
import { Textarea, TextareaProps } from '@/components/textarea'
|
||||
|
||||
type Props<T extends FieldValues> = Omit<
|
||||
UseControllerProps<T>,
|
||||
'control' | 'defaultValue' | 'rules'
|
||||
> &
|
||||
Omit<TextareaProps, 'value' | 'onChange'> & { control: Control<T> }
|
||||
|
||||
export const FormTextarea = <T extends FieldValues>({
|
||||
control,
|
||||
name,
|
||||
disabled,
|
||||
shouldUnregister,
|
||||
...rest
|
||||
}: Props<T>) => {
|
||||
const {
|
||||
field,
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
control,
|
||||
name,
|
||||
disabled,
|
||||
shouldUnregister,
|
||||
})
|
||||
return <Textarea errorMessage={error?.message} {...field} {...rest} />
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Provider } from 'react-redux'
|
||||
import TimeAgo from 'javascript-time-ago'
|
||||
|
||||
import en from 'javascript-time-ago/locale/en'
|
||||
import { Toaster } from 'sonner'
|
||||
|
||||
TimeAgo.addDefaultLocale(en)
|
||||
|
||||
@@ -13,6 +14,7 @@ export default function App({ Component, ...rest }: AppProps) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Layout>
|
||||
<Toaster />
|
||||
<Component {...props.pageProps} />
|
||||
</Layout>
|
||||
</Provider>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
type Data = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
||||
res.status(200).json({ name: 'John Doe123' })
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import Head from 'next/head'
|
||||
import { useController, useForm } from 'react-hook-form'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Input } from '@/components/input'
|
||||
import { Checkbox } from '@/components/checkbox'
|
||||
import { FormCheckbox } from '@/form/form-checkbox'
|
||||
import { FormInput } from '@/form/form-input'
|
||||
import { useLoginMutation } from '@/services/inctagram.auth.service'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useLazyGetUserProfileQuery } from '@/services/inctagram.profile.service'
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(3),
|
||||
rememberMe: z.boolean().optional(),
|
||||
})
|
||||
|
||||
type LoginFields = z.infer<typeof loginSchema>
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter()
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -24,9 +24,20 @@ export default function Login() {
|
||||
resolver: zodResolver(loginSchema),
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit(data => {
|
||||
console.log(data)
|
||||
})
|
||||
const [login] = useLoginMutation()
|
||||
const [getProfile, profile] = useLazyGetUserProfileQuery()
|
||||
const onSubmit = handleSubmit(data =>
|
||||
login(data)
|
||||
.unwrap()
|
||||
.then(async ({ accessToken }) => {
|
||||
localStorage.setItem('token', accessToken)
|
||||
const { data } = await getProfile()
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
void router.push(`/profile/${data?.id}`)
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -34,21 +45,8 @@ export default function Login() {
|
||||
<title>Login | Instagram Client</title>
|
||||
</Head>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormInput name={'email'} control={control} label={'Email'} disabled />
|
||||
<FormInput name={'email'} control={control} label={'Email'} />
|
||||
<FormInput name={'password'} control={control} label={'Password'} type={'password'} />
|
||||
|
||||
<FormCheckbox
|
||||
control={control}
|
||||
name={'rememberMe'}
|
||||
label={
|
||||
<span>
|
||||
I agree to the{' '}
|
||||
<a className={'hover:underline text-sky-500'} href={'#'}>
|
||||
Terms of Service and Privacy Policy
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<button>Send</button>
|
||||
</form>
|
||||
</>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import {
|
||||
useLazyGetUserProfileQuery,
|
||||
useUpdateUserProfileMutation,
|
||||
} from '@/services/inctagram.profile.service'
|
||||
import { FormInput } from '@/form/form-input'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { UpdateUserProfileArgs } from '@/services/inctagram.types'
|
||||
import { useUploadImageMutation } from '@/services/inctagram.service'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Profile() {
|
||||
const [getProfile, profile] = useLazyGetUserProfileQuery()
|
||||
const [files, setFiles] = useState<FileList | null>(null)
|
||||
const [mutate, { isLoading }] = useUpdateUserProfileMutation()
|
||||
const [uploadImage, { isLoading: loadingImage }] = useUploadImageMutation()
|
||||
const { control, handleSubmit } = useForm<UpdateUserProfileArgs>({
|
||||
defaultValues: async () => {
|
||||
const { data } = await getProfile()
|
||||
|
||||
if (!data) {
|
||||
return {} as UpdateUserProfileArgs
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
firstName: data.firstName ?? '',
|
||||
lastName: data.lastName ?? '',
|
||||
dateOfBirth: '1961-07-13T17:58:17.000Z',
|
||||
aboutMe: 'HELLO BITHCHES',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// const onSubmit = handleSubmit(data => {
|
||||
// mutate(data)
|
||||
// })
|
||||
const onSubmit = handleSubmit(mutate)
|
||||
return (
|
||||
<div>
|
||||
Profile
|
||||
<h1 className={'text-xl font-medium'}>{profile.data?.userName}</h1>
|
||||
<h2 className={'text-lg font-medium'}>
|
||||
{profile.data?.firstName} {profile.data?.lastName}
|
||||
</h2>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormInput name={'userName'} control={control} label={'User name'} />
|
||||
<FormInput name={'firstName'} control={control} label={'First name'} />
|
||||
<FormInput name={'lastName'} control={control} label={'Last name'} />
|
||||
<button disabled={isLoading}>Update profile</button>
|
||||
</form>
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault()
|
||||
if (!files || !files.length) {
|
||||
return
|
||||
}
|
||||
uploadImage({ files })
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
multiple
|
||||
onChange={e => {
|
||||
setFiles(e.currentTarget.files)
|
||||
}}
|
||||
/>
|
||||
<button disabled={loadingImage}>upload image</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
14
src/pages/profile/[id]/index.tsx
Normal file
14
src/pages/profile/[id]/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function UserProfile() {
|
||||
const router = useRouter()
|
||||
const id = router.query.id
|
||||
|
||||
return (
|
||||
<div>
|
||||
Profile for user with id {id}
|
||||
<Link href={'/profile/edit'}>Edit Profile</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
113
src/pages/profile/edit.tsx
Normal file
113
src/pages/profile/edit.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
useLazyGetUserProfileQuery,
|
||||
useUpdateUserAvatarMutation,
|
||||
useUpdateUserProfileMutation,
|
||||
} from '@/services/inctagram.profile.service'
|
||||
import { FormInput } from '@/form/form-input'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { UpdateUserProfileArgs } from '@/services/inctagram.types'
|
||||
import { ChangeEvent, FormEvent, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { FormTextarea } from '@/form/form-textarea'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import Link from 'next/link'
|
||||
import { profileSchema, UserProfile } from '@/validation/schemas/profile-schema'
|
||||
import { format } from '@formkit/tempo'
|
||||
|
||||
export default function Profile() {
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
|
||||
const [getProfile, profile] = useLazyGetUserProfileQuery()
|
||||
const [mutate, { isLoading }] = useUpdateUserProfileMutation()
|
||||
const [uploadImage, { isLoading: loadingImage }] = useUpdateUserAvatarMutation()
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useForm<UserProfile>({
|
||||
resolver: zodResolver(profileSchema),
|
||||
defaultValues: async () => {
|
||||
const { data } = await getProfile()
|
||||
|
||||
if (!data) {
|
||||
return {} as UpdateUserProfileArgs
|
||||
}
|
||||
|
||||
const dateOfBirth = data?.dateOfBirth
|
||||
|
||||
return {
|
||||
...data,
|
||||
firstName: data.firstName ?? '',
|
||||
lastName: data.lastName ?? '',
|
||||
dateOfBirth: dateOfBirth ? format(dateOfBirth, 'YYYY-MM-DD') : undefined,
|
||||
aboutMe: data.aboutMe ?? '',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit(data =>
|
||||
toast.promise(mutate(data).unwrap(), {
|
||||
loading: 'Loading...',
|
||||
success: data => {
|
||||
return 'Profile updated successfully'
|
||||
},
|
||||
error: 'Error updating profile',
|
||||
})
|
||||
)
|
||||
|
||||
const handleAvatarFormSubmitted = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
uploadImage({ file })
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
toast.success('Avatar updated successfully')
|
||||
getProfile()
|
||||
})
|
||||
}
|
||||
|
||||
const handleFileInputChanged = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFile(e.currentTarget.files?.[0] ?? null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
Profile
|
||||
<h1 className={'text-xl font-medium'}>{profile.data?.userName}</h1>
|
||||
<h2 className={'text-lg font-medium'}>
|
||||
{profile.data?.firstName} {profile.data?.lastName}
|
||||
</h2>
|
||||
<img src={profile.data?.avatars?.[0].url} className={'rounded-full'} alt={'user profile'} />
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormInput name={'userName'} control={control} label={'User name'} />
|
||||
<FormInput name={'firstName'} control={control} label={'First name'} />
|
||||
<FormInput name={'lastName'} control={control} label={'Last name'} />
|
||||
<input type={'date'} {...register('dateOfBirth')} />
|
||||
{!!errors.dateOfBirth && <UnderageUserError />}
|
||||
<FormInput name={'city'} control={control} label={'City'} />
|
||||
<FormInput name={'country'} control={control} label={'Country'} />
|
||||
<FormTextarea name={'aboutMe'} control={control} label={'About me'} />
|
||||
<button disabled={isLoading}>Update profile</button>
|
||||
</form>
|
||||
<form onSubmit={handleAvatarFormSubmitted}>
|
||||
<input type="file" name="file" onChange={handleFileInputChanged} />
|
||||
<button disabled={loadingImage}>Upload Avatar</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function UnderageUserError() {
|
||||
return (
|
||||
<p className={'text-sm text-red-500'}>
|
||||
A user under 13 cannot create a profile.{' '}
|
||||
<Link href={'#'} className={'underline'}>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
20
src/services/inctagram.auth.service.ts
Normal file
20
src/services/inctagram.auth.service.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { inctagramService } from '@/services/inctagram.service'
|
||||
import { LoginArgs } from '@/services/inctagram.types'
|
||||
|
||||
export const inctagramAuthService = inctagramService.injectEndpoints({
|
||||
endpoints: builder => {
|
||||
return {
|
||||
login: builder.mutation<any, LoginArgs>({
|
||||
query: body => {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: '/v1/auth/login',
|
||||
body,
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const { useLoginMutation } = inctagramAuthService
|
||||
54
src/services/inctagram.fetch-base-query.ts
Normal file
54
src/services/inctagram.fetch-base-query.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
|
||||
import type { BaseQueryFn, FetchArgs, FetchBaseQueryError } from '@reduxjs/toolkit/query'
|
||||
import { Mutex } from 'async-mutex'
|
||||
import Router from 'next/router'
|
||||
// create a new mutex
|
||||
const mutex = new Mutex()
|
||||
const baseQuery = fetchBaseQuery({
|
||||
baseUrl: 'https://inctagram.work/api',
|
||||
prepareHeaders: headers => {
|
||||
const token = localStorage.getItem('token')
|
||||
headers.set('Authorization', `Bearer ${token}`)
|
||||
return headers
|
||||
},
|
||||
})
|
||||
export const baseQueryWithReauth: BaseQueryFn<
|
||||
string | FetchArgs,
|
||||
unknown,
|
||||
FetchBaseQueryError
|
||||
> = async (args, api, extraOptions) => {
|
||||
// wait until the mutex is available without locking it
|
||||
await mutex.waitForUnlock()
|
||||
let result = await baseQuery(args, api, extraOptions)
|
||||
if (result.error && result.error.status === 401) {
|
||||
// checking whether the mutex is locked
|
||||
if (!mutex.isLocked()) {
|
||||
const release = await mutex.acquire()
|
||||
try {
|
||||
const refreshResult = (await baseQuery(
|
||||
{
|
||||
url: '/v1/auth/update-tokens',
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
},
|
||||
api,
|
||||
extraOptions
|
||||
)) as any
|
||||
if (refreshResult.data) {
|
||||
localStorage.setItem('token', refreshResult.data.accessToken)
|
||||
// retry the initial query
|
||||
result = await baseQuery(args, api, extraOptions)
|
||||
} else {
|
||||
console.log('logged out')
|
||||
Router.push('/auth/login')
|
||||
}
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
} else {
|
||||
await mutex.waitForUnlock()
|
||||
result = await baseQuery(args, api, extraOptions)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { inctagramService } from '@/services/inctagram.service'
|
||||
import { GetUserProfileResponse, UpdateUserProfileArgs } from '@/services/inctagram.types'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export const inctagramProfileService = inctagramService.injectEndpoints({
|
||||
endpoints: builder => {
|
||||
@@ -18,10 +19,41 @@ export const inctagramProfileService = inctagramService.injectEndpoints({
|
||||
body,
|
||||
}
|
||||
},
|
||||
async onQueryStarted(args, { dispatch, queryFulfilled }) {
|
||||
const patchResult = dispatch(
|
||||
inctagramProfileService.util.updateQueryData('getUserProfile', undefined, draft => {
|
||||
Object.assign(draft, args)
|
||||
})
|
||||
)
|
||||
try {
|
||||
await queryFulfilled
|
||||
} catch (e) {
|
||||
patchResult.undo()
|
||||
toast.error(
|
||||
e.error.data?.messages?.map(m => m.message).join(', ') ?? 'Something went wrong'
|
||||
)
|
||||
}
|
||||
},
|
||||
invalidatesTags: ['UserProfile'],
|
||||
}),
|
||||
updateUserAvatar: builder.mutation<any, { file: File }>({
|
||||
query: ({ file }) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return {
|
||||
url: '/v1/users/profile/avatar',
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
}
|
||||
},
|
||||
invalidatesTags: ['UserProfile'],
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const { useLazyGetUserProfileQuery, useUpdateUserProfileMutation } = inctagramProfileService
|
||||
export const {
|
||||
useLazyGetUserProfileQuery,
|
||||
useUpdateUserProfileMutation,
|
||||
useUpdateUserAvatarMutation,
|
||||
} = inctagramProfileService
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||
|
||||
const token =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjczNSwiaWF0IjoxNzIxNDE0NTgzLCJleHAiOjE3MjE0MTgxODN9.O1Kao-oNtMDNtCPU0-exGlif7YJegpIVPxeZm1KC758'
|
||||
import { createApi } from '@reduxjs/toolkit/query/react'
|
||||
import { baseQueryWithReauth } from '@/services/inctagram.fetch-base-query'
|
||||
|
||||
export const inctagramService = createApi({
|
||||
// keepUnusedDataFor: 0,
|
||||
tagTypes: ['UserProfile', 'PublicPosts'],
|
||||
reducerPath: 'inctagramService',
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: 'https://inctagram.work/api',
|
||||
prepareHeaders: headers => {
|
||||
headers.set('Authorization', `Bearer ${token}`)
|
||||
return headers
|
||||
},
|
||||
}),
|
||||
baseQuery: baseQueryWithReauth,
|
||||
endpoints: builder => ({
|
||||
uploadImage: builder.mutation<any, { files: FileList }>({
|
||||
query({ files }) {
|
||||
|
||||
@@ -55,6 +55,12 @@ export interface UpdateUserProfileArgs {
|
||||
firstName: string
|
||||
lastName: string
|
||||
city?: string
|
||||
country?: string
|
||||
dateOfBirth?: string
|
||||
aboutMe?: string
|
||||
}
|
||||
|
||||
export type LoginArgs = {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
42
src/validation/schemas/profile-schema.ts
Normal file
42
src/validation/schemas/profile-schema.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { z } from 'zod'
|
||||
import { diffYears } from '@formkit/tempo'
|
||||
|
||||
export const profileSchema = z
|
||||
.object({
|
||||
userName: z
|
||||
.string()
|
||||
.min(6)
|
||||
.max(30)
|
||||
.regex(
|
||||
/^(?![_.-])(?!.*[_.-]{2})[a-zA-Z0-9_-]+([^._-])$/,
|
||||
'Only letters, numbers, dashes and underscores are allowed'
|
||||
),
|
||||
firstName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(50)
|
||||
.regex(/^[A-Za-zА-Яа-я]+$/, 'Only latin and cyrillic letters are allowed'),
|
||||
lastName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(50)
|
||||
.regex(/^[A-Za-zА-Яа-я]+$/, 'Only latin and cyrillic letters are allowed'),
|
||||
dateOfBirth: z.string().date().optional(),
|
||||
city: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
aboutMe: z.string().max(200).optional(),
|
||||
})
|
||||
.superRefine((val, ctx) => {
|
||||
if (val.dateOfBirth) {
|
||||
const diff = diffYears(new Date(), new Date(val.dateOfBirth))
|
||||
console.log(diff)
|
||||
if (diff > 13) return
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['dateOfBirth'],
|
||||
message: 'A user under 13 cannot create a profile. Privacy Policy',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type UserProfile = z.infer<typeof profileSchema>
|
||||
Reference in New Issue
Block a user