mirror of
https://github.com/ershisan99/inctagram-live-2024-08-30.git
synced 2025-12-16 12:33:25 +00:00
lesson 3
This commit is contained in:
@@ -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
124
pnpm-lock.yaml
generated
@@ -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
22
src/components/post.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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}</>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
110
src/pages/profile/[id].tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
102
src/services/instagram.api.ts
Normal file
102
src/services/instagram.api.ts
Normal 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
|
||||
91
src/services/instagram.types.ts
Normal file
91
src/services/instagram.types.ts
Normal 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
33
src/services/store.ts
Normal 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)
|
||||
Reference in New Issue
Block a user