From cd3d836c20855e6dd461b049df106c9b14cedfa0 Mon Sep 17 00:00:00 2001 From: andres Date: Fri, 20 Sep 2024 17:18:18 +0200 Subject: [PATCH] lesson 3 --- package.json | 5 ++ pnpm-lock.yaml | 124 ++++++++++++++++++++++++++++++++ src/components/post.tsx | 22 ++++++ src/pages/_app.tsx | 26 ++++++- src/pages/auth/login.tsx | 43 ++++------- src/pages/index.tsx | 55 ++++++++++++-- src/pages/profile/[id].tsx | 110 ++++++++++++++++++++++++++++ src/services/instagram.api.ts | 102 ++++++++++++++++++++++++++ src/services/instagram.types.ts | 91 +++++++++++++++++++++++ src/services/store.ts | 33 +++++++++ 10 files changed, 574 insertions(+), 37 deletions(-) create mode 100644 src/components/post.tsx create mode 100644 src/pages/profile/[id].tsx create mode 100644 src/services/instagram.api.ts create mode 100644 src/services/instagram.types.ts create mode 100644 src/services/store.ts diff --git a/package.json b/package.json index 78fa372..ea17c00 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d3d7fe..33d1917 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/components/post.tsx b/src/components/post.tsx new file mode 100644 index 0000000..ad58eb5 --- /dev/null +++ b/src/components/post.tsx @@ -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 ( +
+ {post.description} +

{post.userName}

+ +
+ ) +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 2065577..8dc3638 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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 +export default function App({ Component, ...rest }: AppProps) { + const { store, props } = wrapper.useWrappedStore(rest) + return ( + + + + ) +} + +function Me({ children }: PropsWithChildren) { + const { data, isLoading, isError } = useMeQuery() + console.log(data) + if (isLoading) { + return ( +
+

LOADING

+
+ ) + } + return <>{children} } diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index b3340e5..024f354 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -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) => { - 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 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({ 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')} /> - + ) } - -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 diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ef6ad47..50f28cc 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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

Loading...

+ } + + if (isError) { + return

Error!

+ } + + if (!data) { + return null + } + return ( -
- auth - login - sign-up +
+
+ auth + login + sign-up +
+
Registered Users: {data.totalUsers}
+ +
+
    + {data.items.map((post) => { + return ( +
  • + +
  • + ) + })} +
+
) } diff --git a/src/pages/profile/[id].tsx b/src/pages/profile/[id].tsx new file mode 100644 index 0000000..4f40241 --- /dev/null +++ b/src/pages/profile/[id].tsx @@ -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) + 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
Loading
+ } + + const avatarUrl = userProfile?.avatars?.[0]?.url + + return ( +
+
{ + e.preventDefault() + if (!photoToUpload) { + return + } + uploadPhoto({ file: photoToUpload }) + }} + > + { + setPhotoToUpload(e.currentTarget.files?.[0] ?? null) + }} + /> + +
+
+
+ {data && ( +
+
Create post
+
+ {data.images.map((img) => ( + + ))} +
+
{ + e.preventDefault() + const formData = new FormData(e.currentTarget) + + createPost({ + description: formData.get('description') as string, + uploadIds: data.images.map((image) => image.uploadId), + }) + }} + > + + +
+
+ )} +
{userProfile?.userName}
+
{userProfile?.aboutMe}
+ {avatarUrl && } +
    + {userPosts?.items.map((post) => { + return ( +
  • + +
  • + ) + })} +
+
+ ) +} diff --git a/src/services/instagram.api.ts b/src/services/instagram.api.ts new file mode 100644 index 0000000..2538850 --- /dev/null +++ b/src/services/instagram.api.ts @@ -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({ + query: (args) => ({ url: 'v1/auth/login', body: args, method: 'POST' }), + }), + me: builder.query({ + query: () => ({ + url: '/v1/auth/me', + }), + }), + getUserProfile: builder.query({ + query: ({ id }) => ({ + url: `/v1/public-user/profile/${id}`, + }), + }), + getUserPosts: builder.query({ + providesTags: ['Posts'], + query: ({ id }) => ({ + url: `/v1/public-posts/user/${id}`, + }), + }), + uploadFileForPost: builder.mutation({ + 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 diff --git a/src/services/instagram.types.ts b/src/services/instagram.types.ts new file mode 100644 index 0000000..d29c31d --- /dev/null +++ b/src/services/instagram.types.ts @@ -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[] +} diff --git a/src/services/store.ts b/src/services/store.ts new file mode 100644 index 0000000..ad34b22 --- /dev/null +++ b/src/services/store.ts @@ -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 +export type AppState = ReturnType +export type AppDispatch = AppStore['dispatch'] +export type AppThunk = ThunkAction< + ReturnType, + AppState, + unknown, + Action +> + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch = () => useDispatch() +export const useAppSelector: TypedUseSelectorHook = useSelector + +export const wrapper = createWrapper(makeStore)