This commit is contained in:
2024-07-20 12:54:30 +02:00
parent 6c1172afc1
commit 766f336bae
13 changed files with 595 additions and 4 deletions

15
.idea/git_toolbox_prj.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@@ -13,12 +13,18 @@
"@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",
"clsx": "^2.1.1",
"javascript-time-ago": "^2.5.10",
"next": "14.2.5",
"next-redux-wrapper": "^8.1.0",
"prettier": "^3.3.2",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "7.52.1",
"react-redux": "^9.1.2",
"react-time-ago": "^7.3.3",
"zod": "^3.23.8"
},
"devDependencies": {

161
pnpm-lock.yaml generated
View File

@@ -20,12 +20,24 @@ importers:
'@radix-ui/react-icons':
specifier: ^1.3.0
version: 1.3.0(react@18.3.1)
'@reduxjs/toolkit':
specifier: ^2.2.6
version: 2.2.6(react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1))(react@18.3.1)
'@types/javascript-time-ago':
specifier: ^2.0.8
version: 2.0.8
clsx:
specifier: ^2.1.1
version: 2.1.1
javascript-time-ago:
specifier: ^2.5.10
version: 2.5.10
next:
specifier: 14.2.5
version: 14.2.5(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.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1))(react@18.3.1)
prettier:
specifier: ^3.3.2
version: 3.3.2
@@ -38,6 +50,12 @@ importers:
react-hook-form:
specifier: 7.52.1
version: 7.52.1(react@18.3.1)
react-redux:
specifier: ^9.1.2
version: 9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1)
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)
zod:
specifier: ^3.23.8
version: 3.23.8
@@ -329,6 +347,17 @@ packages:
'@types/react':
optional: true
'@reduxjs/toolkit@2.2.6':
resolution: {integrity: sha512-kH0r495c5z1t0g796eDQAkYbEQ3a1OLYN9o8jQQVZyKyw367pfRGS+qZLkHYvFHiUUdafpoSlQ2QYObIApjPWA==}
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.3':
resolution: {integrity: sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==}
@@ -338,6 +367,9 @@ packages:
'@swc/helpers@0.5.5':
resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
'@types/javascript-time-ago@2.0.8':
resolution: {integrity: sha512-X77q3xUzWVn0qohgurKE1G5NiXZjee8VbLqaukW/HXVkz7bdCFJgOPQ3JVB4IkrDhMS4CviFTmpZuNVg0i2QFA==}
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
@@ -353,6 +385,9 @@ packages:
'@types/react@18.3.3':
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
'@types/use-sync-external-store@0.0.3':
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
'@typescript-eslint/parser@7.2.0':
resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -922,6 +957,9 @@ packages:
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
engines: {node: '>= 4'}
immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@@ -1070,6 +1108,9 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
javascript-time-ago@2.5.10:
resolution: {integrity: sha512-EUxp4BP74QH8xiYHyeSHopx1XhMMJ9qEX4rcBdFtpVWmKRdzpxbNzz2GSbuekZr5wt0rmLehuyp0PE34EAJT9g==}
jiti@1.21.6:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
@@ -1137,6 +1178,9 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -1180,6 +1224,13 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next-redux-wrapper@8.1.0:
resolution: {integrity: sha512-2hIau0hcI6uQszOtrvAFqgc0NkZegKYhBB7ZAKiG3jk7zfuQb4E7OV9jfxViqqojh3SEHdnFfPkN9KErttUKuw==}
peerDependencies:
next: '>=9'
react: '*'
react-redux: '*'
next@14.2.5:
resolution: {integrity: sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==}
engines: {node: '>=18.17.0'}
@@ -1291,6 +1342,9 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
@@ -1379,6 +1433,9 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
raf@3.4.1:
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
@@ -1393,6 +1450,25 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
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-time-ago@7.3.3:
resolution: {integrity: sha512-5kh2Kuu/UhHzcZrGvf3GUrF2d+IXjkIXif5MR2iDWIfSqQuBW27/ejN/tmzJBRyPiryYTgbDIG6AZFJ4RW3yfw==}
peerDependencies:
javascript-time-ago: ^2.3.7
react: '>=0.16.8'
react-dom: '>=0.16.8'
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -1404,6 +1480,14 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
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'}
@@ -1412,6 +1496,12 @@ packages:
resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
engines: {node: '>= 0.4'}
relative-time-format@1.1.6:
resolution: {integrity: sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==}
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'}
@@ -1643,6 +1733,11 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
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==}
@@ -1906,6 +2001,16 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.3
'@reduxjs/toolkit@2.2.6(react-redux@9.1.2(@types/react@18.3.3)(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.3)(react@18.3.1)(redux@5.0.1)
'@rushstack/eslint-patch@1.10.3': {}
'@swc/counter@0.1.3': {}
@@ -1915,6 +2020,8 @@ snapshots:
'@swc/counter': 0.1.3
tslib: 2.6.3
'@types/javascript-time-ago@2.0.8': {}
'@types/json5@0.0.29': {}
'@types/node@20.14.10':
@@ -1932,6 +2039,8 @@ snapshots:
'@types/prop-types': 15.7.12
csstype: 3.1.3
'@types/use-sync-external-store@0.0.3': {}
'@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.3)':
dependencies:
'@typescript-eslint/scope-manager': 7.2.0
@@ -2729,6 +2838,8 @@ snapshots:
ignore@5.3.1: {}
immer@10.1.1: {}
import-fresh@3.3.0:
dependencies:
parent-module: 1.0.1
@@ -2876,6 +2987,10 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
javascript-time-ago@2.5.10:
dependencies:
relative-time-format: 1.1.6
jiti@1.21.6: {}
js-tokens@4.0.0: {}
@@ -2934,6 +3049,8 @@ snapshots:
lru-cache@10.4.3: {}
memoize-one@6.0.0: {}
merge2@1.4.1: {}
micromatch@4.0.7:
@@ -2971,6 +3088,12 @@ snapshots:
natural-compare@1.4.0: {}
next-redux-wrapper@8.1.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1))(react@18.3.1):
dependencies:
next: 14.2.5(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.3)(react@18.3.1)(redux@5.0.1)
next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 14.2.5
@@ -3091,6 +3214,8 @@ snapshots:
path-type@4.0.0: {}
performance-now@2.1.0: {}
picocolors@1.0.1: {}
picomatch@2.3.1: {}
@@ -3160,6 +3285,10 @@ snapshots:
queue-microtask@1.2.3: {}
raf@3.4.1:
dependencies:
performance-now: 2.1.0
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
@@ -3172,6 +3301,24 @@ snapshots:
react-is@16.13.1: {}
react-redux@9.1.2(@types/react@18.3.3)(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.3
redux: 5.0.1
react-time-ago@7.3.3(javascript-time-ago@2.5.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
javascript-time-ago: 2.5.10
memoize-one: 6.0.0
prop-types: 15.8.1
raf: 3.4.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react@18.3.1:
dependencies:
loose-envify: 1.4.0
@@ -3184,6 +3331,12 @@ snapshots:
dependencies:
picomatch: 2.3.1
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
@@ -3201,6 +3354,10 @@ snapshots:
es-errors: 1.3.0
set-function-name: 2.0.2
relative-time-format@1.1.6: {}
reselect@5.1.1: {}
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -3484,6 +3641,10 @@ snapshots:
dependencies:
punycode: 2.3.1
use-sync-external-store@1.2.2(react@18.3.1):
dependencies:
react: 18.3.1
util-deprecate@1.0.2: {}
which-boxed-primitive@1.0.2:

View File

@@ -0,0 +1,92 @@
.loader {
position: relative;
width: 130px;
height: 100px;
background-repeat: no-repeat;
background-image: linear-gradient(#0277bd, #0277bd), linear-gradient(#29b6f6, #4fc3f7),
linear-gradient(#29b6f6, #4fc3f7);
background-size:
80px 70px,
30px 50px,
30px 30px;
background-position:
0 0,
80px 20px,
100px 40px;
}
.loader:after {
content: '';
position: absolute;
bottom: 10px;
left: 12px;
width: 10px;
height: 10px;
background: #fff;
border-radius: 50%;
box-sizing: content-box;
border: 10px solid #000;
box-shadow:
78px 0 0 -10px #fff,
78px 0 #000;
animation: wheelSk 0.75s ease-in infinite alternate;
}
.loader:before {
content: '';
position: absolute;
right: 100%;
top: 0px;
height: 70px;
width: 70px;
background-image: linear-gradient(#fff 45px, transparent 0),
linear-gradient(#fff 45px, transparent 0), linear-gradient(#fff 45px, transparent 0);
background-repeat: no-repeat;
background-size: 30px 4px;
background-position:
0px 11px,
8px 35px,
0px 60px;
animation: lineDropping 0.75s linear infinite;
}
@keyframes wheelSk {
0%,
50%,
100% {
transform: translatey(0);
}
30%,
90% {
transform: translatey(-3px);
}
}
@keyframes lineDropping {
0% {
background-position:
100px 11px,
115px 35px,
105px 60px;
opacity: 1;
}
50% {
background-position:
0px 11px,
20px 35px,
5px 60px;
}
60% {
background-position:
-30px 11px,
0px 35px,
-10px 60px;
}
75%,
100% {
background-position:
-30px 11px,
-30px 35px,
-30px 60px;
opacity: 0;
}
}

View File

@@ -0,0 +1,5 @@
import s from './loader.module.css'
export const Loader = () => {
return <span className={s.loader}></span>
}

View File

@@ -1,11 +1,21 @@
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import { wrapper } from '@/store'
import { Provider } from 'react-redux'
import TimeAgo from 'javascript-time-ago'
export default function App({ Component, pageProps }: AppProps) {
import en from 'javascript-time-ago/locale/en'
TimeAgo.addDefaultLocale(en)
export default function App({ Component, ...rest }: AppProps) {
const { store, props } = wrapper.useWrappedStore(rest)
return (
<Layout>
<Component {...pageProps} />
</Layout>
<Provider store={store}>
<Layout>
<Component {...props.pageProps} />
</Layout>
</Provider>
)
}

View File

@@ -1,5 +1,9 @@
import Head from 'next/head'
import Link from 'next/link'
import ReactTimeAgo from 'react-time-ago'
import { Loader } from '@/components/loader/loader'
import { useState } from 'react'
import { useGetPublicPostsQuery } from '@/services/inctagram.public-posts.service'
export default function Home() {
return (
@@ -13,11 +17,54 @@ export default function Home() {
}
function Content() {
const [pageSize, setPageSize] = useState(4)
const { data, isLoading, isError, error } = useGetPublicPostsQuery({
pageSize,
})
if (isLoading) {
return (
<div className={'w-screen h-screen grid place-items-center'}>
<Loader />
</div>
)
}
if (isError) {
return <div>Error: {JSON.stringify(error, null, 2)}</div>
}
return (
<div>
<nav>
<Link href={'/auth/login'}>Login</Link>
</nav>
<select value={pageSize} onChange={e => setPageSize(Number.parseInt(e.target.value, 10))}>
<option value={4}>4</option>
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
<div className={'grid grid-cols-4 p-6 mx-auto gap-4'}>
{data?.items?.map(post => {
return (
<div key={post.id} className={'w-60'}>
<img src={post.images[0].url} className={'w-full aspect-square object-cover'} />
<div className={'flex gap-3 items-center mt-3'}>
<img
src={post.avatarOwner ?? 'https://picsum.photos/200'}
className={'size-9 rounded-full object-cover'}
/>
<div>{post.userName}</div>
</div>
<ReactTimeAgo
date={new Date(post.createdAt)}
className={'mt-3 text-sm text-gray-500'}
/>
{post.description && <div className={'mt-4'}>{post.description}</div>}
</div>
)
})}
</div>
</div>
)
}

72
src/pages/profile.tsx Normal file
View File

@@ -0,0 +1,72 @@
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>
)
}

View File

@@ -0,0 +1,27 @@
import { inctagramService } from '@/services/inctagram.service'
import { GetUserProfileResponse, UpdateUserProfileArgs } from '@/services/inctagram.types'
export const inctagramProfileService = inctagramService.injectEndpoints({
endpoints: builder => {
return {
getUserProfile: builder.query<GetUserProfileResponse, void>({
query: () => {
return 'v1/users/profile'
},
providesTags: ['UserProfile'],
}),
updateUserProfile: builder.mutation<any, UpdateUserProfileArgs>({
query: body => {
return {
url: '/v1/users/profile',
method: 'PUT',
body,
}
},
invalidatesTags: ['UserProfile'],
}),
}
},
})
export const { useLazyGetUserProfileQuery, useUpdateUserProfileMutation } = inctagramProfileService

View File

@@ -0,0 +1,29 @@
import { inctagramService } from '@/services/inctagram.service'
import {
GetPostsArgs,
GetPostsResponse,
GetUserProfileResponse,
UpdateUserProfileArgs,
} from '@/services/inctagram.types'
export const inctagramPublicPostsService = inctagramService.injectEndpoints({
endpoints: builder => {
return {
getPublicPosts: builder.query<GetPostsResponse, GetPostsArgs>({
query: ({ pageSize }) => {
return {
url: `v1/public-posts/all/`,
params: {
pageSize,
sortBy: 'createdAt',
sortDirection: 'desc',
},
}
},
providesTags: ['PublicPosts'],
}),
}
},
})
export const { useGetPublicPostsQuery } = inctagramPublicPostsService

View File

@@ -0,0 +1,44 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjczNSwiaWF0IjoxNzIxNDE0NTgzLCJleHAiOjE3MjE0MTgxODN9.O1Kao-oNtMDNtCPU0-exGlif7YJegpIVPxeZm1KC758'
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
},
}),
endpoints: builder => ({
uploadImage: builder.mutation<any, { files: FileList }>({
query({ files }) {
const formData = new FormData()
Array.from(files).forEach(file => {
formData.append('file', file)
})
console.log(formData)
return {
method: 'POST',
body: formData,
url: '/v1/posts/image',
}
},
}),
deleteImage: builder.mutation<any, { id: string }>({
query({ id }) {
return {
method: 'DELETE',
url: `/v1/posts/image/${id}`,
params: {},
}
},
}),
}),
})
export const { useUploadImageMutation } = inctagramService

View File

@@ -0,0 +1,60 @@
export interface GetPostsResponse {
totalCount: number
pageSize: number
items: Item[]
totalUsers: number
}
export interface GetPostsArgs {
pageSize: number
}
export interface Item {
id: number
userName: string
description: string
location?: any
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 GetUserProfileResponse {
id: number
userName: string
firstName?: any
lastName?: any
city?: any
dateOfBirth?: any
aboutMe?: any
createdAt: string
avatars: any[]
}
export interface UpdateUserProfileArgs {
userName: string
firstName: string
lastName: string
city?: string
dateOfBirth?: string
aboutMe?: string
}

23
src/store.ts Normal file
View File

@@ -0,0 +1,23 @@
import { Action, configureStore, ThunkAction } from '@reduxjs/toolkit'
import { createWrapper } from 'next-redux-wrapper'
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import { inctagramService } from '@/services/inctagram.service'
const makeStore = () =>
configureStore({
reducer: {
[inctagramService.reducerPath]: inctagramService.reducer,
},
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(inctagramService.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>
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector
export const wrapper = createWrapper<AppStore>(makeStore)