This commit is contained in:
2024-09-20 17:18:18 +02:00
parent 6a65074c16
commit cd3d836c20
10 changed files with 574 additions and 37 deletions

View File

@@ -16,13 +16,18 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@reduxjs/toolkit": "^2.2.7",
"@types/react-timeago": "^4.1.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.439.0",
"next": "14.2.7",
"next-redux-wrapper": "^8.1.0",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.53.0",
"react-redux": "^9.1.2",
"react-timeago": "^7.2.0",
"tailwind-merge": "^2.5.2",
"zod": "^3.23.8"
},

124
pnpm-lock.yaml generated
View File

@@ -23,6 +23,12 @@ importers:
'@radix-ui/react-slot':
specifier: ^1.1.0
version: 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@reduxjs/toolkit':
specifier: ^2.2.7
version: 2.2.7(react-redux@9.1.2(@types/react@18.3.5)(react@18.3.1)(redux@5.0.1))(react@18.3.1)
'@types/react-timeago':
specifier: ^4.1.7
version: 4.1.7
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
@@ -35,6 +41,9 @@ importers:
next:
specifier: 14.2.7
version: 14.2.7(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-redux-wrapper:
specifier: ^8.1.0
version: 8.1.0(next@14.2.7(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-redux@9.1.2(@types/react@18.3.5)(react@18.3.1)(redux@5.0.1))(react@18.3.1)
react:
specifier: ^18
version: 18.3.1
@@ -44,6 +53,12 @@ importers:
react-hook-form:
specifier: ^7.53.0
version: 7.53.0(react@18.3.1)
react-redux:
specifier: ^9.1.2
version: 9.1.2(@types/react@18.3.5)(react@18.3.1)(redux@5.0.1)
react-timeago:
specifier: ^7.2.0
version: 7.2.0(react@18.3.1)
tailwind-merge:
specifier: ^2.5.2
version: 2.5.2
@@ -1503,6 +1518,17 @@ packages:
'@radix-ui/rect@1.1.0':
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
'@reduxjs/toolkit@2.2.7':
resolution: {integrity: sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
peerDependenciesMeta:
react:
optional: true
react-redux:
optional: true
'@rushstack/eslint-patch@1.10.4':
resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==}
@@ -1845,6 +1871,9 @@ packages:
'@types/react-dom@18.3.0':
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
'@types/react-timeago@4.1.7':
resolution: {integrity: sha512-ogD4Ror/hDG+pQggCX+TgPgJ8W2jeeUxsgNU485Qpm0Ma+E2TND2EJuKwK5+sxlkDXDEgsHradO0zWBkTgLzNg==}
'@types/react@18.3.5':
resolution: {integrity: sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==}
@@ -1863,6 +1892,9 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/use-sync-external-store@0.0.3':
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
'@types/uuid@9.0.8':
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
@@ -3334,6 +3366,9 @@ packages:
engines: {node: '>=16.x'}
hasBin: true
immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@@ -3864,6 +3899,13 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
next-redux-wrapper@8.1.0:
resolution: {integrity: sha512-2hIau0hcI6uQszOtrvAFqgc0NkZegKYhBB7ZAKiG3jk7zfuQb4E7OV9jfxViqqojh3SEHdnFfPkN9KErttUKuw==}
peerDependencies:
next: '>=9'
react: '*'
react-redux: '*'
next@14.2.7:
resolution: {integrity: sha512-4Qy2aK0LwH4eQiSvQWyKuC7JXE13bIopEQesWE0c/P3uuNRnZCQanI0vsrMLmUQJLAto+A+/8+sve2hd+BQuOQ==}
engines: {node: '>=18.17.0'}
@@ -4369,6 +4411,18 @@ packages:
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-redux@9.1.2:
resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==}
peerDependencies:
'@types/react': ^18.2.25
react: ^18.0
redux: ^5.0.0
peerDependenciesMeta:
'@types/react':
optional: true
redux:
optional: true
react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
@@ -4403,6 +4457,11 @@ packages:
'@types/react':
optional: true
react-timeago@7.2.0:
resolution: {integrity: sha512-2KsBEEs+qRhKx/kekUVNSTIpop3Jwd7SRBm0R4Eiq3mPeswRGSsftY9FpKsE/lXLdURyQFiHeHFrIUxLYskG5g==}
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -4433,6 +4492,14 @@ packages:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
redux-thunk@3.1.0:
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
peerDependencies:
redux: ^5.0.0
redux@5.0.1:
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
reflect.getprototypeof@1.0.6:
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
engines: {node: '>= 0.4'}
@@ -4486,6 +4553,9 @@ packages:
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
engines: {node: '>=0.10.5'}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -5080,6 +5150,11 @@ packages:
'@types/react':
optional: true
use-sync-external-store@1.2.2:
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -6675,6 +6750,16 @@ snapshots:
'@radix-ui/rect@1.1.0': {}
'@reduxjs/toolkit@2.2.7(react-redux@9.1.2(@types/react@18.3.5)(react@18.3.1)(redux@5.0.1))(react@18.3.1)':
dependencies:
immer: 10.1.1
redux: 5.0.1
redux-thunk: 3.1.0(redux@5.0.1)
reselect: 5.1.1
optionalDependencies:
react: 18.3.1
react-redux: 9.1.2(@types/react@18.3.5)(react@18.3.1)(redux@5.0.1)
'@rushstack/eslint-patch@1.10.4': {}
'@sinclair/typebox@0.27.8': {}
@@ -7242,6 +7327,10 @@ snapshots:
dependencies:
'@types/react': 18.3.5
'@types/react-timeago@4.1.7':
dependencies:
'@types/react': 18.3.5
'@types/react@18.3.5':
dependencies:
'@types/prop-types': 15.7.12
@@ -7264,6 +7353,8 @@ snapshots:
'@types/unist@3.0.3': {}
'@types/use-sync-external-store@0.0.3': {}
'@types/uuid@9.0.8': {}
'@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4)':
@@ -9137,6 +9228,8 @@ snapshots:
dependencies:
queue: 6.0.2
immer@10.1.1: {}
import-fresh@3.3.0:
dependencies:
parent-module: 1.0.1
@@ -9618,6 +9711,12 @@ snapshots:
neo-async@2.6.2: {}
next-redux-wrapper@8.1.0(next@14.2.7(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-redux@9.1.2(@types/react@18.3.5)(react@18.3.1)(redux@5.0.1))(react@18.3.1):
dependencies:
next: 14.2.7(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-redux: 9.1.2(@types/react@18.3.5)(react@18.3.1)(redux@5.0.1)
next@14.2.7(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 14.2.7
@@ -10161,6 +10260,15 @@ snapshots:
react-is@18.3.1: {}
react-redux@9.1.2(@types/react@18.3.5)(react@18.3.1)(redux@5.0.1):
dependencies:
'@types/use-sync-external-store': 0.0.3
react: 18.3.1
use-sync-external-store: 1.2.2(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.5
redux: 5.0.1
react-refresh@0.14.2: {}
react-remove-scroll-bar@2.3.6(@types/react@18.3.5)(react@18.3.1):
@@ -10191,6 +10299,10 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.5
react-timeago@7.2.0(react@18.3.1):
dependencies:
react: 18.3.1
react@18.3.1:
dependencies:
loose-envify: 1.4.0
@@ -10240,6 +10352,12 @@ snapshots:
indent-string: 4.0.0
strip-indent: 3.0.0
redux-thunk@3.1.0(redux@5.0.1):
dependencies:
redux: 5.0.1
redux@5.0.1: {}
reflect.getprototypeof@1.0.6:
dependencies:
call-bind: 1.0.7
@@ -10315,6 +10433,8 @@ snapshots:
requireindex@1.2.0: {}
reselect@5.1.1: {}
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -11002,6 +11122,10 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.5
use-sync-external-store@1.2.2(react@18.3.1):
dependencies:
react: 18.3.1
util-deprecate@1.0.2: {}
util@0.12.5:

22
src/components/post.tsx Normal file
View File

@@ -0,0 +1,22 @@
import TimeAgo from 'react-timeago'
type PostProps = {
images: Array<{ url: string }>
description: string
userName: string
createdAt: string
}
export function Post(post: PostProps) {
return (
<div>
<img
className={'object-cover size-60'}
src={post.images[0].url}
alt={post.description}
/>
<p>{post.userName}</p>
<TimeAgo date={new Date(post.createdAt)} />
</div>
)
}

View File

@@ -2,7 +2,29 @@ import '@fontsource/inter/400.css'
import '@fontsource/inter/700.css'
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import { wrapper } from '../services/store'
import { Provider } from 'react-redux'
import { PropsWithChildren } from 'react'
import { useMeQuery } from '@/services/instagram.api'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
export default function App({ Component, ...rest }: AppProps) {
const { store, props } = wrapper.useWrappedStore(rest)
return (
<Provider store={store}>
<Component {...props.pageProps} />
</Provider>
)
}
function Me({ children }: PropsWithChildren) {
const { data, isLoading, isError } = useMeQuery()
console.log(data)
if (isLoading) {
return (
<div className={'h-screen grid place-items-center'}>
<h1>LOADING</h1>
</div>
)
}
return <>{children}</>
}

View File

@@ -2,6 +2,8 @@ import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { TextField } from '@/components/ui/text-field/text-field'
import { useLoginMutation } from '@/services/instagram.api'
import { useRouter } from 'next/router'
const loginSchema = z.object({
email: z
@@ -14,21 +16,11 @@ const loginSchema = z.object({
.min(3, 'Минимум 3 символа'),
})
const createLoginSchema = (t: ReturnType<typeof useI18n>) => {
return z.object({
email: z.string().min(1, 'Required').email(t('ERROR_INVALID_EMAIL')),
password: z
.string({ required_error: 'Required' })
.min(1, 'Required')
.min(3, 'Минимум 3 символа'),
})
}
type LoginFields = z.infer<typeof loginSchema>
export default function Login() {
const t = useI18n()
const [logIn, { data, isLoading, isError }] = useLoginMutation()
const router = useRouter()
const {
handleSubmit,
register,
@@ -36,9 +28,15 @@ export default function Login() {
} = useForm<LoginFields>({
resolver: zodResolver(loginSchema),
})
console.log({ data, isError })
const onSubmit = handleSubmit((data) => {
console.log(data)
logIn(data)
.unwrap()
.then((data) => {
localStorage.setItem('access_token', data.accessToken)
router.push('/')
})
})
return (
@@ -61,25 +59,8 @@ export default function Login() {
{...register('password')}
/>
<button>Sign In</button>
<button disabled={isLoading}>Sign In</button>
</form>
</div>
)
}
const useI18n = () => {
const lang = 'ru'
return (key: keyof (typeof translations)['ru']) => {
return translations[lang][key]
}
}
const translations = {
ru: {
ERROR_INVALID_EMAIL: 'Введите валидный адрес эл. почты',
},
en: {
ERROR_INVALID_EMAIL: 'Invalid email',
},
} as const

View File

@@ -1,11 +1,58 @@
import Link from 'next/link'
import { useGetAllPublicPostsQuery } from '@/services/instagram.api'
import { useState } from 'react'
import TimeAgo from 'react-timeago'
import { Post } from '@/components/post'
export default function Home() {
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
const { data, isLoading, isError } = useGetAllPublicPostsQuery({
pageSize: 4,
sortBy: 'userName',
sortDirection: sortDirection,
})
if (isLoading) {
return <h1>Loading...</h1>
}
if (isError) {
return <h1>Error!</h1>
}
if (!data) {
return null
}
return (
<div className={'p-10 flex flex-col gap-4'}>
<Link href={'/auth'}>auth</Link>
<Link href={'/auth/login'}>login</Link>
<Link href={'/auth/sign-up'}>sign-up</Link>
<div className={'p-10 flex flex-col gap-4 bg-'}>
<header className={'flex gap-3'}>
<Link href={'/auth'}>auth</Link>
<Link href={'/auth/login'}>login</Link>
<Link href={'/auth/sign-up'}>sign-up</Link>
</header>
<section>Registered Users: {data.totalUsers}</section>
<select
value={sortDirection}
onChange={(e) => setSortDirection(e.target.value as any)}
>
<option value={'asc'}>asc</option>
<option value={'desc'}>desc</option>
</select>
<section>
<ul className={'flex gap-4 flex-wrap'}>
{data.items.map((post) => {
return (
<li
className={'w-1/5'}
key={post.id}
>
<Post {...post} />
</li>
)
})}
</ul>
</section>
</div>
)
}

110
src/pages/profile/[id].tsx Normal file
View File

@@ -0,0 +1,110 @@
import { useRouter } from 'next/router'
import {
useCreatePostMutation,
useGetUserPostsQuery,
useGetUserProfileQuery,
useUploadFileForPostMutation,
} from '@/services/instagram.api'
import { Post } from '@/components/post'
import { useState } from 'react'
export default function UserProfile() {
const [photoToUpload, setPhotoToUpload] = useState<null | File>(null)
const [uploadPhoto, { data }] = useUploadFileForPostMutation()
const [createPost] = useCreatePostMutation()
console.log(photoToUpload)
const route = useRouter()
const userId = route.query.id
const { data: userProfile, isLoading: isUserProfileLoading } =
useGetUserProfileQuery(
{
id: parseInt(userId as string, 10),
},
{
skip: userId === undefined,
}
)
const { data: userPosts, isLoading: isUserPostsLoading } =
useGetUserPostsQuery(
{
id: parseInt(userId as string, 10),
},
{
skip: userId === undefined,
}
)
if (isUserPostsLoading || isUserProfileLoading) {
return <div>Loading</div>
}
const avatarUrl = userProfile?.avatars?.[0]?.url
return (
<div>
<form
onSubmit={(e) => {
e.preventDefault()
if (!photoToUpload) {
return
}
uploadPhoto({ file: photoToUpload })
}}
>
<input
type={'file'}
onChange={(e) => {
setPhotoToUpload(e.currentTarget.files?.[0] ?? null)
}}
/>
<button>Upload Photo</button>
</form>
<hr />
<hr />
{data && (
<div>
<div>Create post</div>
<div>
{data.images.map((img) => (
<img src={img.url} />
))}
</div>
<form
onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
createPost({
description: formData.get('description') as string,
uploadIds: data.images.map((image) => image.uploadId),
})
}}
>
<input
type={'text'}
name='description'
/>
<button>Create post</button>
</form>
</div>
)}
<div>{userProfile?.userName}</div>
<div>{userProfile?.aboutMe}</div>
{avatarUrl && <img src={avatarUrl} />}
<ul className={'flex gap-4 flex-wrap'}>
{userPosts?.items.map((post) => {
return (
<li
className={'w-1/5'}
key={post.id}
>
<Post {...post} />
</li>
)
})}
</ul>
</div>
)
}

View File

@@ -0,0 +1,102 @@
// Need to use the React-specific entry point to import createApi
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import {
GetAllPostsArgs,
GetAllPostsResponse,
LoginArgs,
LoginResponse,
MeResponse,
UploadFileResponse,
UserPosts,
UserProfile,
} from '@/services/instagram.types'
// Define a service using a base URL and expected endpoints
export const instagramApi = createApi({
tagTypes: ['Posts'],
reducerPath: 'instagramApi',
baseQuery: fetchBaseQuery({
baseUrl: 'https://inctagram.work/api/',
prepareHeaders: (headers) => {
const token = localStorage.getItem('access_token')
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
},
}),
endpoints: (builder) => ({
getAllPublicPosts: builder.query<
GetAllPostsResponse,
GetAllPostsArgs | void
>({
query: (arg) => {
const { endCursorPostId, ...params } = arg ?? {}
return { url: `/v1/public-posts/all/${endCursorPostId}`, params }
},
}),
login: builder.mutation<LoginResponse, LoginArgs>({
query: (args) => ({ url: 'v1/auth/login', body: args, method: 'POST' }),
}),
me: builder.query<MeResponse, void>({
query: () => ({
url: '/v1/auth/me',
}),
}),
getUserProfile: builder.query<UserProfile, { id: number }>({
query: ({ id }) => ({
url: `/v1/public-user/profile/${id}`,
}),
}),
getUserPosts: builder.query<UserPosts, { id: number }>({
providesTags: ['Posts'],
query: ({ id }) => ({
url: `/v1/public-posts/user/${id}`,
}),
}),
uploadFileForPost: builder.mutation<UploadFileResponse, { file: File }>({
query: ({ file }) => {
const formData = new FormData()
formData.append('file', file)
return {
url: '/v1/posts/image',
body: formData,
method: 'POST',
}
},
}),
createPost: builder.mutation<
any,
{ description: string; uploadIds: string[] }
>({
invalidatesTags: ['Posts'],
query: ({ description, uploadIds }) => {
return {
url: '/v1/posts',
body: {
description,
childrenMetadata: uploadIds.map((id) => {
return {
uploadId: id,
}
}),
},
method: 'POST',
}
},
}),
}),
})
// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const {
useGetAllPublicPostsQuery,
useLoginMutation,
useMeQuery,
useGetUserProfileQuery,
useGetUserPostsQuery,
useUploadFileForPostMutation,
useCreatePostMutation,
} = instagramApi

View File

@@ -0,0 +1,91 @@
export interface UserProfile {
id: number
userName: string
firstName: string
lastName: string
city: string
country: string
region: string
dateOfBirth: string
aboutMe: string
avatars: Avatar[]
createdAt: string
}
export interface Avatar {
url: string
width: number
height: number
fileSize: number
createdAt: string
}
export interface MeResponse {
userId: number
userName: string
email: string
isBlocked: boolean
}
export interface LoginArgs {
email: string
password: string
}
export interface LoginResponse {
accessToken: string
}
export interface GetAllPostsArgs {
pageSize?: number
sortBy?: string
sortDirection?: 'asc' | 'desc'
endCursorPostId?: number
}
export interface GetAllPostsResponse {
totalCount: number
pageSize: number
items: Post[]
totalUsers: number
}
export interface Post {
id: number
userName: string
description: string
location?: string | null
images: Image[]
createdAt: string
updatedAt: string
avatarOwner?: string
ownerId: number
owner: Owner
likesCount: number
isLiked: boolean
}
export interface Owner {
firstName: string
lastName: string
}
export interface Image {
url: string
width: number
height: number
fileSize: number
createdAt: string
uploadId: string
}
export interface UserPosts {
totalCount: number
pageSize: number
items: Post[]
totalUsers: number
}
export interface UploadFileResponse {
images: Image[]
}

33
src/services/store.ts Normal file
View File

@@ -0,0 +1,33 @@
import {
Action,
combineSlices,
configureStore,
ThunkAction,
} from '@reduxjs/toolkit'
import { createWrapper } from 'next-redux-wrapper'
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import { instagramApi } from '@/services/instagram.api'
const makeStore = () =>
configureStore({
reducer: combineSlices(instagramApi),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(instagramApi.middleware),
devTools: true,
})
export type AppStore = ReturnType<typeof makeStore>
export type AppState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
AppState,
unknown,
Action
>
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector
export const wrapper = createWrapper<AppStore>(makeStore)