diff --git a/Dockerfile b/Dockerfile
index 888cd3c7..d8443ce6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,15 @@
-FROM node:18.15 as dependencies
+FROM node:20.11-alpine as dependencies
WORKDIR /app
-COPY package.json package-lock.json ./
+COPY package*.json ./
RUN npm install
-FROM node:18.15 as builder
+FROM node:20.11-alpine as builder
WORKDIR /app
COPY . .
COPY --from=dependencies /app/node_modules ./node_modules
RUN npm run build:production
-FROM node:18.15 as runner
+FROM node:20.11-alpine as runner
WORKDIR /app
ENV NODE_ENV production
# If you are using a custom next.config.js file, uncomment this line.
diff --git a/Jenkinsfile b/Jenkinsfile
index 8e0349fe..1b0e6604 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -4,12 +4,12 @@ pipeline {
agent any
environment {
ENV_TYPE = "production"
- PORT = 3162
- NAMESPACE = "incta-online"
+ PORT = 3534
+ NAMESPACE = "mypicto-ru"
REGISTRY_HOSTNAME = "elem15ten"
REGISTRY = "registry.hub.docker.com"
- PROJECT = "incta-online"
- DEPLOYMENT_NAME = "incta-online-deployment"
+ PROJECT = "incta"
+ DEPLOYMENT_NAME = "incta-deployment"
IMAGE_NAME = "${env.BUILD_ID}_${env.ENV_TYPE}_${env.GIT_COMMIT}"
DOCKER_BUILD_NAME = "${env.REGISTRY_HOSTNAME}/${env.PROJECT}:${env.IMAGE_NAME}"
}
@@ -33,7 +33,7 @@ pipeline {
steps {
echo "Push image started..."
script {
- docker.withRegistry("https://${env.REGISTRY}", 'incta-online') {
+ docker.withRegistry("https://${env.REGISTRY}", 'mypicto-ru') {
app.push("${env.IMAGE_NAME}")
}
}
diff --git a/next.config.js b/next.config.js
index 53e098d1..fea7d477 100644
--- a/next.config.js
+++ b/next.config.js
@@ -34,6 +34,12 @@ const nextConfig = {
port: '',
pathname: '/trainee-instagram-api/**',
},
+ {
+ protocol: 'https',
+ hostname: 'staging-it-incubator.s3.eu-central-1.amazonaws.com',
+ port: '',
+ pathname: '/trainee-instagram-api/Image/**',
+ },
],
},
}
diff --git a/nginx.conf b/nginx.conf
index 83a0931c..ad9f82b6 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -1,6 +1,6 @@
server {
- listen 3162;
+ listen 3534;
server_name localhost;
access_log /var/log/nginx/host.access.log;
error_log /var/log/nginx/host.error.log;
@@ -53,7 +53,7 @@ server {
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
- add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-FollowersAndFollowing,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
+ add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
}
}
diff --git a/package-lock.json b/package-lock.json
index f6b4f393..72c97b54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "inctagram",
"version": "0.1.0",
"dependencies": {
+ "@apollo/client": "^3.10.8",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
@@ -19,6 +20,7 @@
"date-fns": "^2.30.0",
"framer-motion": "^10.16.4",
"get-orientation": "^1.1.2",
+ "graphql": "^16.9.0",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.289.0",
"next": "^13.4.19",
@@ -35,6 +37,8 @@
"react-select-async-paginate": "^0.7.3",
"react-time-ago": "^7.2.1",
"sharp": "^0.32.6",
+ "socket.io-client": "^4.7.5",
+ "subscriptions-transport-ws": "^0.11.0",
"swiper": "^11.0.5",
"uuid": "^9.0.1"
},
@@ -115,6 +119,48 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@apollo/client": {
+ "version": "3.10.8",
+ "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.10.8.tgz",
+ "integrity": "sha512-UaaFEitRrPRWV836wY2L7bd3HRCfbMie1jlYMcmazFAK23MVhz/Uq7VG1nwbotPb5xzFsw5RF4Wnp2G3dWPM3g==",
+ "dependencies": {
+ "@graphql-typed-document-node/core": "^3.1.1",
+ "@wry/caches": "^1.0.0",
+ "@wry/equality": "^0.5.6",
+ "@wry/trie": "^0.5.0",
+ "graphql-tag": "^2.12.6",
+ "hoist-non-react-statics": "^3.3.2",
+ "optimism": "^0.18.0",
+ "prop-types": "^15.7.2",
+ "rehackt": "^0.1.0",
+ "response-iterator": "^0.2.6",
+ "symbol-observable": "^4.0.0",
+ "ts-invariant": "^0.10.3",
+ "tslib": "^2.3.0",
+ "zen-observable-ts": "^1.2.5"
+ },
+ "peerDependencies": {
+ "graphql": "^15.0.0 || ^16.0.0",
+ "graphql-ws": "^5.5.5",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "subscriptions-transport-ws": "^0.9.0 || ^0.11.0"
+ },
+ "peerDependenciesMeta": {
+ "graphql-ws": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "subscriptions-transport-ws": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@aw-web-design/x-default-browser": {
"version": "1.4.126",
"resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz",
@@ -3020,6 +3066,14 @@
"integrity": "sha512-qF0aH5UiZvCmneX5orJbVRoc2VTyLTV3X/7laMp03Qt28L+B9tFlZODOGUL64wDWc69YVdi1LeJB0cIgd51lvw==",
"dev": true
},
+ "node_modules/@graphql-typed-document-node/core": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
+ "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
+ "peerDependencies": {
+ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -4858,6 +4912,11 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true
},
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
+ },
"node_modules/@storybook/addon-actions": {
"version": "7.6.8",
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.8.tgz",
@@ -7790,6 +7849,50 @@
"@xtuc/long": "4.2.2"
}
},
+ "node_modules/@wry/caches": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz",
+ "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@wry/context": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz",
+ "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@wry/equality": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz",
+ "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@wry/trie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz",
+ "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -8716,6 +8819,11 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
+ "node_modules/backo2": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+ "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA=="
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -10783,6 +10891,26 @@
"objectorarray": "^1.0.5"
}
},
+ "node_modules/engine.io-client": {
+ "version": "6.5.4",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
+ "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1",
+ "xmlhttprequest-ssl": "~2.0.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
@@ -11853,6 +11981,11 @@
"node": ">=6"
}
},
+ "node_modules/eventemitter3": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
+ "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
+ },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -13023,6 +13156,28 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "node_modules/graphql": {
+ "version": "16.9.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
+ "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==",
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
+ "node_modules/graphql-tag": {
+ "version": "2.12.6",
+ "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz",
+ "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
+ }
+ },
"node_modules/gunzip-maybe": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz",
@@ -14133,6 +14288,11 @@
"node": ">=8"
}
},
+ "node_modules/iterall": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz",
+ "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg=="
+ },
"node_modules/iterator.prototype": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
@@ -15937,6 +16097,28 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/optimism": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.0.tgz",
+ "integrity": "sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ==",
+ "dependencies": {
+ "@wry/caches": "^1.0.0",
+ "@wry/context": "^0.7.0",
+ "@wry/trie": "^0.4.3",
+ "tslib": "^2.3.0"
+ }
+ },
+ "node_modules/optimism/node_modules/@wry/trie": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.4.3.tgz",
+ "integrity": "sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -17838,6 +18020,23 @@
"jsesc": "bin/jsesc"
}
},
+ "node_modules/rehackt": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz",
+ "integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "*"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
@@ -18000,6 +18199,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/response-iterator": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz",
+ "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@@ -18616,6 +18823,32 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/socket.io-client": {
+ "version": "4.7.5",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
+ "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
@@ -19317,6 +19550,50 @@
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
+ "node_modules/subscriptions-transport-ws": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz",
+ "integrity": "sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==",
+ "deprecated": "The `subscriptions-transport-ws` package is no longer maintained. We recommend you use `graphql-ws` instead. For help migrating Apollo software to `graphql-ws`, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws For general help using `graphql-ws`, see https://github.com/enisdenjo/graphql-ws/blob/master/README.md",
+ "dependencies": {
+ "backo2": "^1.0.2",
+ "eventemitter3": "^3.1.0",
+ "iterall": "^1.2.1",
+ "symbol-observable": "^1.0.4",
+ "ws": "^5.2.0 || ^6.0.0 || ^7.0.0"
+ },
+ "peerDependencies": {
+ "graphql": "^15.7.2 || ^16.0.0"
+ }
+ },
+ "node_modules/subscriptions-transport-ws/node_modules/symbol-observable": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/subscriptions-transport-ws/node_modules/ws": {
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -19517,6 +19794,14 @@
"node": ">= 4.7.0"
}
},
+ "node_modules/symbol-observable": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
+ "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/synchronous-promise": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.17.tgz",
@@ -20129,6 +20414,17 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true
},
+ "node_modules/ts-invariant": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz",
+ "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ts-pnp": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
@@ -21173,10 +21469,9 @@
}
},
"node_modules/ws": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
- "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
- "dev": true,
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
@@ -21193,6 +21488,14 @@
}
}
},
+ "node_modules/xmlhttprequest-ssl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
+ "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -21246,6 +21549,19 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zen-observable": {
+ "version": "0.8.15",
+ "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
+ "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="
+ },
+ "node_modules/zen-observable-ts": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz",
+ "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==",
+ "dependencies": {
+ "zen-observable": "0.8.15"
+ }
}
}
}
diff --git a/package.json b/package.json
index b1bc76d1..7023467a 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
+ "@apollo/client": "^3.10.8",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
@@ -27,6 +28,7 @@
"date-fns": "^2.30.0",
"framer-motion": "^10.16.4",
"get-orientation": "^1.1.2",
+ "graphql": "^16.9.0",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.289.0",
"next": "^13.4.19",
@@ -40,12 +42,13 @@
"react-hook-form": "^7.47.0",
"react-redux": "^8.1.3",
"react-select": "^5.7.7",
- "uuid": "^9.0.1",
"react-select-async-paginate": "^0.7.3",
"react-time-ago": "^7.2.1",
"sharp": "^0.32.6",
- "swiper": "^11.0.5"
-
+ "socket.io-client": "^4.7.5",
+ "subscriptions-transport-ws": "^0.11.0",
+ "swiper": "^11.0.5",
+ "uuid": "^9.0.1"
},
"devDependencies": {
"@fontsource/inter": "^5.0.14",
diff --git a/pages/more-information/index.ts b/pages/more-information/index.ts
new file mode 100644
index 00000000..ce708ac1
--- /dev/null
+++ b/pages/more-information/index.ts
@@ -0,0 +1 @@
+export { MoreInformationPage as default } from '@/pages/moreInformation'
diff --git a/pages/paymentsList/index.tsx b/pages/paymentsList/index.tsx
new file mode 100644
index 00000000..bd77e0dd
--- /dev/null
+++ b/pages/paymentsList/index.tsx
@@ -0,0 +1 @@
+export { PaymentsListPage as default } from '@/pages/paymentsList'
diff --git a/pages/postsList/index.tsx b/pages/postsList/index.tsx
new file mode 100644
index 00000000..8dec5de4
--- /dev/null
+++ b/pages/postsList/index.tsx
@@ -0,0 +1 @@
+export { PostsListPage as default } from '@/pages/postsList'
diff --git a/pages/search/index.tsx b/pages/search/index.tsx
index e07473ed..8037cdf4 100644
--- a/pages/search/index.tsx
+++ b/pages/search/index.tsx
@@ -1 +1 @@
-export { Search as default } from '@/pages/search'
+export { SearchPage as default } from '@/pages/search'
diff --git a/pages/superAdmin/index.tsx b/pages/superAdmin/index.tsx
new file mode 100644
index 00000000..0a58f7f5
--- /dev/null
+++ b/pages/superAdmin/index.tsx
@@ -0,0 +1 @@
+export { AdminPage as default } from '@/pages/superAdmin'
diff --git a/pages/userList/index.tsx b/pages/userList/index.tsx
new file mode 100644
index 00000000..7348ed45
--- /dev/null
+++ b/pages/userList/index.tsx
@@ -0,0 +1 @@
+export { UserListPage as default } from '@/pages/userList'
diff --git a/pages/userProfile/[name]/index.ts b/pages/userProfile/[name]/index.ts
new file mode 100644
index 00000000..77f37afa
--- /dev/null
+++ b/pages/userProfile/[name]/index.ts
@@ -0,0 +1 @@
+export { UserProfilePage as default } from '@/pages/userProfilePage'
diff --git "a/public/icons/icons8-\321\202\321\200\320\270-\321\202\320\276\321\207\320\272\320\270-48.png" "b/public/icons/icons8-\321\202\321\200\320\270-\321\202\320\276\321\207\320\272\320\270-48.png"
new file mode 100644
index 00000000..a03cf3db
Binary files /dev/null and "b/public/icons/icons8-\321\202\321\200\320\270-\321\202\320\276\321\207\320\272\320\270-48.png" differ
diff --git a/public/icons/thre-dots-white.png b/public/icons/thre-dots-white.png
new file mode 100644
index 00000000..27e98167
Binary files /dev/null and b/public/icons/thre-dots-white.png differ
diff --git a/src/app/appStore.ts b/src/app/appStore.ts
index 6e3a24cd..1ea60c47 100644
--- a/src/app/appStore.ts
+++ b/src/app/appStore.ts
@@ -4,11 +4,18 @@ import { TypedUseSelectorHook, useSelector } from 'react-redux'
import { authReducer, authApi } from '../entities/auth'
import { appSlice, postSlice } from '@/app/services'
+import { adminSlice } from '@/app/services/admin-slice'
import { croppersSlice } from '@/app/services/cropper-slice'
+import { commentsApi } from '@/entities/comments'
import { countriesApi } from '@/entities/countries/'
+import { devicesApi } from "@/entities/device's"
+import { notificationsApi } from '@/entities/notifications/api/notificationsApi'
import { postsApi } from '@/entities/posts'
import { profileApi } from '@/entities/profile'
import { publicPostsApi } from '@/entities/publicPosts'
+import { subscriptionApi } from '@/entities/subscription'
+import { usersApi } from '@/entities/users/api/usersApi'
+import { usersFollowApi } from '@/entities/users-follow/api/usersFollowApi'
const store = configureStore({
reducer: {
@@ -17,11 +24,18 @@ const store = configureStore({
[postSlice.name]: postSlice.reducer,
[authApi.reducerPath]: authApi.reducer,
[croppersSlice.name]: croppersSlice.reducer,
+ [adminSlice.name]: adminSlice.reducer,
// [authGoogleApi.reducerPath]: authGoogleApi.reducer,
[profileApi.reducerPath]: profileApi.reducer,
[countriesApi.reducerPath]: countriesApi.reducer,
[publicPostsApi.reducerPath]: publicPostsApi.reducer,
[postsApi.reducerPath]: postsApi.reducer,
+ [subscriptionApi.reducerPath]: subscriptionApi.reducer,
+ [devicesApi.reducerPath]: devicesApi.reducer,
+ [usersApi.reducerPath]: usersApi.reducer,
+ [notificationsApi.reducerPath]: notificationsApi.reducer,
+ [usersFollowApi.reducerPath]: usersFollowApi.reducer,
+ [commentsApi.reducerPath]: commentsApi.reducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(
@@ -30,7 +44,14 @@ const store = configureStore({
profileApi.middleware,
countriesApi.middleware,
publicPostsApi.middleware,
- postsApi.middleware
+ postsApi.middleware,
+ subscriptionApi.middleware,
+ devicesApi.middleware,
+ usersApi.middleware,
+ notificationsApi.middleware,
+ usersFollowApi.middleware,
+ notificationsApi.middleware,
+ commentsApi.middleware
),
})
diff --git a/src/app/index.tsx b/src/app/index.tsx
index fa2d599c..9dc29a04 100644
--- a/src/app/index.tsx
+++ b/src/app/index.tsx
@@ -20,7 +20,7 @@ import { ReduxProvider } from './providers'
import { useLoader } from '@/shared/lib'
import { NotificationContainer } from '@/widgets/alertContainer'
-TimeAgo.addDefaultLocale(en)
+TimeAgo.addLocale(en)
TimeAgo.addLocale(ru)
export type NextPageWithLayout
= NextPage
& {
diff --git a/src/app/services/admin-slice.ts b/src/app/services/admin-slice.ts
new file mode 100644
index 00000000..d319461e
--- /dev/null
+++ b/src/app/services/admin-slice.ts
@@ -0,0 +1,22 @@
+import { PayloadAction, createSlice } from '@reduxjs/toolkit'
+
+import { RootState } from '../appStore'
+type InitType = {
+ isAdmin: boolean
+}
+
+const initialState: InitType = {
+ isAdmin: false,
+}
+
+export const adminSlice = createSlice({
+ initialState,
+ name: 'adminSlice',
+ reducers: {
+ isAdmin: (state, action: PayloadAction) => {
+ state.isAdmin = action.payload
+ },
+ },
+})
+export const { isAdmin } = adminSlice.actions
+export const selectIsAdmin = (state: RootState) => state.adminSlice.isAdmin
diff --git a/src/app/services/cropper-slice.ts b/src/app/services/cropper-slice.ts
index 8a9478fb..ce51a4c5 100644
--- a/src/app/services/cropper-slice.ts
+++ b/src/app/services/cropper-slice.ts
@@ -26,18 +26,22 @@ export const croppersSlice = createSlice({
name: 'croppersSlice',
reducers: {
addNewPhoto: (state, action: PayloadAction) => {
- const newData: CropperState = {
- id: v1(),
- image: action.payload,
- crop: { x: 0, y: 0 },
- zoom: 1,
- croppedAreaPixels: null,
- filterClass: '',
- aspect: 1,
- originalImage: '',
- }
+ const existingPhoto = state.find(cropper => cropper.image === action.payload)
+
+ if (!existingPhoto) {
+ const newData: CropperState = {
+ id: v1(),
+ image: action.payload,
+ crop: { x: 0, y: 0 },
+ zoom: 1,
+ croppedAreaPixels: null,
+ filterClass: '',
+ aspect: 1,
+ originalImage: '',
+ }
- state.unshift(newData)
+ state.unshift(newData)
+ }
},
deletePhoto: (state, action: PayloadAction) => {
const imageIndex = state.findIndex(image => image.id === action.payload)
diff --git a/src/entities/auth/api/authApi.ts b/src/entities/auth/api/authApi.ts
index 1fe7d0cc..3bfdbb18 100644
--- a/src/entities/auth/api/authApi.ts
+++ b/src/entities/auth/api/authApi.ts
@@ -1,7 +1,6 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
-import { clearLocalUserData, setLoginUser } from '../model/authSlice'
-
+import { clearLocalUserData, setLoginUser } from '@/entities/auth'
import { BACKEND_URL, BASE_WORK_URL } from '@/shared/constants/ext-urls'
import { consoleErrors } from '@/shared/lib'
import { IEmailBaseUrl, IEmailPassword, IEmailPasswordUser } from '@/shared/types'
@@ -86,6 +85,26 @@ export const authApi = createApi({
}
},
}),
+
+ loginAdmin: builder.mutation({
+ query: credentials => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ mutation {
+ loginAdmin(email: "${credentials.email}", password: "${credentials.password}") {
+ logged
+ }
+ }
+ `,
+ }),
+ }),
+ }),
+
refreshToken: builder.mutation({
query: () => ({
url: '/auth/update-tokens',
@@ -137,6 +156,7 @@ export const authApi = createApi({
method: 'POST',
}),
}),
+
googleLogin: builder.mutation({
query: code => ({
body: { code },
@@ -164,6 +184,7 @@ export const {
useRegistrationMutation,
useRegistrationConfirmationMutation,
useLoginMutation,
+ useLoginAdminMutation,
useSendCaptchaMutation,
useCreateNewPasswordMutation,
useValidCodeMutation,
diff --git a/src/entities/auth/index.ts b/src/entities/auth/index.ts
index d6b579a4..a182061b 100644
--- a/src/entities/auth/index.ts
+++ b/src/entities/auth/index.ts
@@ -4,6 +4,7 @@ export {
useRegistrationMutation,
useRegistrationConfirmationMutation,
useLoginMutation,
+ useLoginAdminMutation,
useSendCaptchaMutation,
useCreateNewPasswordMutation,
useValidCodeMutation,
diff --git a/src/entities/comments/api/commentsApi.ts b/src/entities/comments/api/commentsApi.ts
new file mode 100644
index 00000000..e718b6c3
--- /dev/null
+++ b/src/entities/comments/api/commentsApi.ts
@@ -0,0 +1,148 @@
+import { createApi } from '@reduxjs/toolkit/query/react'
+
+import { baseQueryWithReauth } from '@/entities/posts'
+
+export const commentsApi = createApi({
+ reducerPath: 'comments',
+ baseQuery: baseQueryWithReauth,
+ tagTypes: ['Comments', 'PublicComments'],
+ endpoints: builder => ({
+ updateComment: builder.mutation<
+ any,
+ {
+ content: string
+ postId: number | undefined
+ accessToken: string | undefined
+ }
+ >({
+ query: ({ content, postId, accessToken }) => {
+ return {
+ url: `/posts/${postId}/comments`,
+ body: { content },
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }
+ },
+ invalidatesTags: ['Comments'],
+ }),
+ getComment: builder.query({
+ query: ({ postId, accessToken }) => {
+ return {
+ method: 'GET',
+ url: `/posts/${postId}/comments`,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }
+ },
+ providesTags: ['Comments'],
+ }),
+ getCommentUnAuthorization: builder.query({
+ query: ({ postId }) => {
+ return {
+ method: 'GET',
+ url: `/public-posts/${postId}/comments`,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }
+ },
+ providesTags: ['Comments'],
+ }),
+
+ getAnswer: builder.query({
+ query: ({ postId, commentId, accessToken }) => {
+ return {
+ method: 'GET',
+ url: `/posts/${postId}/comments/${commentId}/answers`,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }
+ },
+ providesTags: ['Comments'],
+ }),
+ likeComment: builder.mutation<
+ any,
+ {
+ likeStatus: string
+ postId: number | undefined
+ commentId: number
+ accessToken: string | undefined
+ }
+ >({
+ query: ({ commentId, postId, accessToken, likeStatus }) => {
+ return {
+ url: `/posts/${postId}/comments/${commentId}/like-status`,
+ body: { likeStatus },
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }
+ },
+ invalidatesTags: ['Comments'],
+ }),
+ likeAnswer: builder.mutation<
+ any,
+ {
+ likeStatus: string
+ postId: number | undefined
+ commentId: number
+ accessToken: string | undefined
+ answerId: number
+ }
+ >({
+ query: ({ commentId, answerId, postId, accessToken, likeStatus }) => {
+ return {
+ url: `/posts/${postId}/comments/${commentId}/answers/${answerId}/like-status`,
+ body: { likeStatus },
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }
+ },
+ invalidatesTags: ['Comments'],
+ }),
+ createAnswer: builder.mutation<
+ any,
+ {
+ content: string | undefined
+ commentId: number | undefined
+ postId: number | undefined
+ accessToken: string | undefined
+ }
+ >({
+ query: ({ content, postId, accessToken, commentId }) => {
+ return {
+ url: `/posts/${postId}/comments/${commentId}/answers`,
+ body: { content },
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }
+ },
+ invalidatesTags: ['Comments'],
+ }),
+ }),
+})
+
+export const {
+ useUpdateCommentMutation,
+ useGetCommentQuery,
+ useGetCommentUnAuthorizationQuery,
+ useLikeCommentMutation,
+ useCreateAnswerMutation,
+ useGetAnswerQuery,
+ useLikeAnswerMutation,
+} = commentsApi
diff --git a/src/entities/comments/index.ts b/src/entities/comments/index.ts
new file mode 100644
index 00000000..50a9c9d7
--- /dev/null
+++ b/src/entities/comments/index.ts
@@ -0,0 +1,9 @@
+export { baseQueryWithReauth } from '../posts/api/baseQueryWithReauth'
+
+export {
+ commentsApi,
+ useUpdateCommentMutation,
+ useGetCommentQuery,
+ useGetCommentUnAuthorizationQuery,
+ useLikeCommentMutation,
+} from './api/commentsApi'
diff --git a/src/entities/device's/api/devicesApi.ts b/src/entities/device's/api/devicesApi.ts
new file mode 100644
index 00000000..1192b8ba
--- /dev/null
+++ b/src/entities/device's/api/devicesApi.ts
@@ -0,0 +1,57 @@
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
+
+import { baseQueryWithReauth } from '@/entities/posts'
+import { OptionsType } from '@/shared/components'
+import { BASE_URL } from '@/shared/constants/ext-urls'
+
+export const devicesApi = createApi({
+ reducerPath: 'apiDevices',
+ baseQuery: baseQueryWithReauth,
+ tagTypes: ['Devices'],
+ endpoints: builder => ({
+ getDevices: builder.query({
+ query: ({ accessToken }) => ({
+ method: 'GET',
+ url: '/sessions',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ providesTags: ['Devices'],
+ }),
+ deleteAll: builder.mutation({
+ query: ({ accessToken }) => {
+ return {
+ url: '/sessions/terminate-all',
+ method: 'DELETE',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }
+ },
+ invalidatesTags: ['Devices'],
+ }),
+ deleteSession: builder.mutation<
+ void,
+ { deviceId: number | undefined; accessToken: string | undefined }
+ >({
+ query: ({ deviceId, accessToken }) => {
+ return {
+ url: `/sessions/${deviceId}`,
+ method: 'DELETE',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }
+ },
+ invalidatesTags: ['Devices'],
+ }),
+ }),
+})
+
+export const { useGetDevicesQuery, useDeleteAllMutation, useDeleteSessionMutation } = devicesApi
diff --git a/src/entities/device's/index.ts b/src/entities/device's/index.ts
new file mode 100644
index 00000000..d3db2f51
--- /dev/null
+++ b/src/entities/device's/index.ts
@@ -0,0 +1,6 @@
+export {
+ useDeleteAllMutation,
+ useDeleteSessionMutation,
+ useGetDevicesQuery,
+ devicesApi,
+} from './api/devicesApi'
diff --git a/src/entities/notifications/api/notificationsApi.ts b/src/entities/notifications/api/notificationsApi.ts
new file mode 100644
index 00000000..f77ff5b5
--- /dev/null
+++ b/src/entities/notifications/api/notificationsApi.ts
@@ -0,0 +1,38 @@
+import { createApi } from '@reduxjs/toolkit/query/react'
+
+import { baseQueryWithReauth } from '@/entities/posts'
+
+export const notificationsApi = createApi({
+ reducerPath: 'notifications',
+ baseQuery: baseQueryWithReauth,
+ tagTypes: ['notifications'],
+ endpoints: builder => ({
+ getNotifications: builder.query({
+ query: accessToken => ({
+ method: 'GET',
+ url: '/notifications?pageSize=120',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ }),
+ updateNotifications: builder.mutation({
+ query: ({ body, accessToken }) => {
+ return {
+ method: 'PUT',
+ url: '/notifications/mark-as-read',
+ credentials: 'include',
+ body,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ invalidatesTags: ['notifications'],
+ }
+ },
+ }),
+ }),
+})
+
+export const { useGetNotificationsQuery, useUpdateNotificationsMutation } = notificationsApi
diff --git a/src/entities/notifications/index.ts b/src/entities/notifications/index.ts
new file mode 100644
index 00000000..6a04feaf
--- /dev/null
+++ b/src/entities/notifications/index.ts
@@ -0,0 +1 @@
+export { useGetNotificationsQuery, useUpdateNotificationsMutation } from './api/notificationsApi'
diff --git a/src/entities/posts/api/postsApi.ts b/src/entities/posts/api/postsApi.ts
index d70154a7..b95bc163 100644
--- a/src/entities/posts/api/postsApi.ts
+++ b/src/entities/posts/api/postsApi.ts
@@ -2,6 +2,7 @@ import { createApi } from '@reduxjs/toolkit/query/react'
import { baseQueryWithReauth } from '..'
+import { transformCommentsData, transformPostData } from '@/entities/publicPosts/api/publicPostsApi'
import { getLargeImage } from '@/shared/lib'
export const postsApi = createApi({
@@ -118,6 +119,27 @@ export const postsApi = createApi({
},
invalidatesTags: [],
}),
+ getPostOfFollowers: builder.query({
+ query: ({ accessToken }) => ({
+ url: `/home/publications-followers`,
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ providesTags: ['Posts'],
+ transformResponse: (response: PublicPostsResponseData) => {
+ const publicPostsData = response?.items.map(transformPostData)
+
+ return {
+ items: publicPostsData,
+ totalUsers: response.totalUsers,
+ totalCount: response.totalCount,
+ pageSize: response.pageSize,
+ }
+ },
+ }),
}),
})
@@ -127,4 +149,5 @@ export const {
usePublishPostsMutation,
useUpdatePostMutation,
useDeletePostMutation,
+ useGetPostOfFollowersQuery,
} = postsApi
diff --git a/src/entities/publicPosts/api/publicPostsApi.ts b/src/entities/publicPosts/api/publicPostsApi.ts
index 8b4d7b2e..c368dacf 100644
--- a/src/entities/publicPosts/api/publicPostsApi.ts
+++ b/src/entities/publicPosts/api/publicPostsApi.ts
@@ -2,7 +2,7 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { BACKEND_URL } from '@/shared/constants/ext-urls'
-const transformPostData = (el: PostDataType): PostDataType => {
+export const transformPostData = (el: PostDataType): PostDataType => {
return {
id: el.id,
ownerId: el.ownerId,
@@ -12,6 +12,26 @@ const transformPostData = (el: PostDataType): PostDataType => {
avatarOwner: el.avatarOwner,
updatedAt: el.updatedAt,
userName: el.userName,
+ createdAt: el.createdAt,
+ }
+}
+export const transformCommentsData = (el: CommentsDataType): CommentsDataType => {
+ return {
+ id: el.id,
+ postId: el.postId,
+ from: el.from,
+ content: el.content,
+ createdAt: el.createdAt,
+ isLiked: el.isLiked,
+ likeCount: el.likeCount,
+ }
+}
+export const transformAnswerData = (el: any): any => {
+ return {
+ id: el.id,
+ postId: el.postId,
+ from: el.from,
+ content: el.content,
}
}
diff --git a/src/entities/socket/socket-api.ts b/src/entities/socket/socket-api.ts
new file mode 100644
index 00000000..ad6e3af6
--- /dev/null
+++ b/src/entities/socket/socket-api.ts
@@ -0,0 +1,23 @@
+import { io, Socket } from 'socket.io-client'
+
+export class SocketApi {
+ static socket: null | Socket = null
+
+ static creatConnection(token: string) {
+ const socketOptions = {
+ query: {
+ accessToken: token,
+ },
+ }
+
+ this.socket = io('https://inctagram.work', socketOptions)
+
+ this.socket.on('connect', () => {
+ console.log('socket connected')
+ })
+
+ this.socket.on('disconnect', e => {
+ console.log('socket disconnected', e)
+ })
+ }
+}
diff --git a/src/entities/subscription/api/subscriptionApi.ts b/src/entities/subscription/api/subscriptionApi.ts
new file mode 100644
index 00000000..bc3116e6
--- /dev/null
+++ b/src/entities/subscription/api/subscriptionApi.ts
@@ -0,0 +1,63 @@
+import { createApi } from '@reduxjs/toolkit/query/react'
+
+import { baseQueryWithReauth } from '@/entities/posts'
+import { ISubscriptionBodyWithToken } from '@/shared/types'
+
+export const subscriptionApi = createApi({
+ reducerPath: 'subscription',
+ baseQuery: baseQueryWithReauth,
+ tagTypes: ['subscription'],
+ endpoints: builder => ({
+ subscribe: builder.mutation<{ url: string }, ISubscriptionBodyWithToken>({
+ query: ({ body, accessToken }) => ({
+ url: '/subscriptions',
+ method: 'POST',
+ credentials: 'include',
+ body,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ invalidatesTags: ['subscription'],
+ }),
+ currentSubscription: builder.query({
+ query: accessToken => ({
+ method: 'GET',
+ url: `/subscriptions/current-payment-subscriptions`,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ }),
+ getPayments: builder.query({
+ query: accessToken => ({
+ method: 'GET',
+ url: `/subscriptions/my-payments`,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ }),
+ autoRenewal: builder.mutation({
+ query: accessToken => ({
+ url: '/subscriptions/canceled-auto-renewal',
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ }),
+ }),
+})
+
+export const {
+ useSubscribeMutation,
+ useAutoRenewalMutation,
+ useCurrentSubscriptionQuery,
+ useGetPaymentsQuery,
+} = subscriptionApi
diff --git a/src/entities/subscription/index.ts b/src/entities/subscription/index.ts
new file mode 100644
index 00000000..6e8a1a58
--- /dev/null
+++ b/src/entities/subscription/index.ts
@@ -0,0 +1 @@
+export { subscriptionApi, useSubscribeMutation } from './api/subscriptionApi'
diff --git a/src/entities/users-follow/api/usersFollowApi.ts b/src/entities/users-follow/api/usersFollowApi.ts
new file mode 100644
index 00000000..7750b618
--- /dev/null
+++ b/src/entities/users-follow/api/usersFollowApi.ts
@@ -0,0 +1,71 @@
+import { createApi } from '@reduxjs/toolkit/query/react'
+
+import { baseQueryWithReauth } from '@/entities/posts'
+
+export const usersFollowApi = createApi({
+ reducerPath: 'userFollow',
+ baseQuery: baseQueryWithReauth,
+ tagTypes: ['Users'],
+ endpoints: builder => ({
+ getUsersName: builder.query<
+ SearchUsers,
+ { name: string | null; accessToken: string | undefined }
+ >({
+ query: ({ name, accessToken }) => ({
+ method: 'GET',
+ url: `/users/?search=${name}`,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ providesTags: ['Users'],
+ }),
+ getUserName: builder.query<
+ UserProfile,
+ { name: string | null; accessToken: string | undefined }
+ >({
+ query: ({ name, accessToken }) => ({
+ method: 'GET',
+ url: `/users/${name}`,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ providesTags: ['Users'],
+ }),
+ following: builder.mutation({
+ query: ({ userId, accessToken }) => ({
+ method: 'POST',
+ url: '/users/following',
+ body: {
+ selectedUserId: userId,
+ },
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ invalidatesTags: ['Users'],
+ }),
+ unFollowing: builder.mutation({
+ query: ({ userId, accessToken }) => ({
+ method: 'DELETE',
+ url: `/users/follower/${userId}`,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + accessToken,
+ },
+ }),
+ invalidatesTags: ['Users'],
+ }),
+ }),
+})
+
+export const {
+ useGetUsersNameQuery,
+ useGetUserNameQuery,
+ useFollowingMutation,
+ useUnFollowingMutation,
+} = usersFollowApi
diff --git a/src/entities/users/api/usersApi.ts b/src/entities/users/api/usersApi.ts
new file mode 100644
index 00000000..75e83c1f
--- /dev/null
+++ b/src/entities/users/api/usersApi.ts
@@ -0,0 +1,293 @@
+import { createApi } from '@reduxjs/toolkit/dist/query/react'
+
+import { baseQueryWithReauth } from '@/entities/posts'
+
+export const usersApi = createApi({
+ reducerPath: 'apiUsers',
+ baseQuery: baseQueryWithReauth,
+ tagTypes: ['Users'],
+ refetchOnMountOrArgChange: true,
+ endpoints: builder => ({
+ getUser: builder.mutation({
+ query: id => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ query {
+ getUser(
+ userId: ${id}
+ ) {
+ id, userName, email, createdAt, profile {id, userName, firstName, lastName, city, dateOfBirth, aboutMe, createdAt, avatars {url, width, height, fileSize}}, userBan {reason, createdAt}
+ }
+ }
+ `,
+ }),
+ }),
+ }),
+ getUsers: builder.mutation({
+ query: data => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ query {
+ getUsers(
+ pageSize: ${data.pageSize},
+ pageNumber: ${data.pageNumber},
+ sortBy: "${data.sortBy}",
+ sortDirection: ${data.sortDirection},
+ searchTerm: "${data.searchTerm}",
+ statusFilter: ${data.statusFilter}
+ ) {
+ users {id, userName, email, createdAt, profile {id, userName, firstName, lastName, city, dateOfBirth, aboutMe, createdAt, avatars {url, width, height, fileSize}}, userBan {reason, createdAt}}
+ pagination {pagesCount page pageSize totalCount}
+ }
+ }
+ `,
+ }),
+ }),
+ invalidatesTags: ['Users'],
+ }),
+ getPostsByUser: builder.mutation({
+ query: data => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ query {
+ getPostsByUser(
+ userId: ${data.id},
+ endCursorId: ${data.endCursorId}
+ ) {
+ pagesCount, pageSize, totalCount, items {id, createdAt, url, width, height, fileSize}
+ }
+ }
+ `,
+ }),
+ }),
+ }),
+ deleteUser: builder.mutation({
+ query: data => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ mutation {
+ removeUser(userId : ${data.userId})
+ }
+ `,
+ }),
+ }),
+ }),
+
+ unBanUser: builder.mutation({
+ query: data => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ mutation {
+ unbanUser(
+ userId : ${data.userId})
+ }
+ `,
+ }),
+ }),
+ }),
+ getPaymentsLIst: builder.mutation({
+ query: data => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ query {
+ getPayments(
+ pageSize: ${data.pageSize},
+ pageNumber: ${data.pageNumber},
+ sortBy: "${data.sortBy}",
+ sortDirection: ${data.sortDirection},
+ searchTerm: "${data.searchTerm}"
+ ) {
+ pagesCount, page, pageSize, totalCount, items {
+ id, userId, paymentMethod, amount, currency, createdAt, endDate, type, userName, avatars {
+ url, width, height, fileSize
+ }
+ }
+ }
+ }
+ `,
+ }),
+ }),
+ }),
+ getPaymentsByUser: builder.mutation({
+ query: data => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ query {
+ getPaymentsByUser(
+ userId: ${data.userId},
+ pageSize: ${data.pageSize},
+ pageNumber: ${data.pageNumber},
+ sortBy: "${data.sortBy}",
+ sortDirection: ${data.sortDirection}
+ ) {
+ pagesCount, page, pageSize, totalCount, items {
+ id, businessAccountId, status, dateOfPayment, startDate, endDate, type, price, paymentType, payments {
+ id, userId, paymentMethod, amount, currency, createdAt, endDate, type
+ }
+ }
+ }
+ }
+ `,
+ }),
+ }),
+ }),
+ getPostsList: builder.mutation({
+ query: data => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ query {
+ getPosts(
+ endCursorPostId: ${data.endCursorPostId},
+ searchTerm: "${data.searchTerm}",
+ pageSize: ${data.pageSize},
+ sortBy: "${data.sortBy}",
+ sortDirection: ${data.sortDirection},
+ ) {
+ pagesCount, pageSize, totalCount, items {
+ images {id, createdAt, url, width, height, fileSize}, id, ownerId, description, createdAt, updatedAt, postOwner {
+ id, userName, firstName, lastName, avatars {url, width, height, fileSize}
+ }
+ }
+ }
+ }
+ `,
+ }),
+ }),
+ }),
+ getFollowers: builder.mutation({
+ query: data => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ query {
+ getFollowers(
+ pageSize: ${data.pageSize}
+ pageNumber: ${data.pageNumber}
+ sortBy: "${data.sortBy}"
+ sortDirection: ${data.sortDirection}
+ userId: ${data.userId}
+ ) {
+ pagesCount, page, pageSize, totalCount, items {
+ id, userId, userName, createdAt
+ }
+ }
+ }
+ `,
+ }),
+ }),
+ }),
+ getFollowing: builder.mutation({
+ query: data => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ query {
+ getFollowing(
+ pageSize: ${data.pageSize}
+ pageNumber: ${data.pageNumber}
+ sortBy: "${data.sortBy}"
+ sortDirection: ${data.sortDirection}
+ userId: ${data.userId}
+ ) {
+ pagesCount, page, pageSize, totalCount, items {
+ id, userId, userName, createdAt
+ }
+ }
+ }
+ `,
+ }),
+ }),
+ }),
+ banUser: builder.mutation({
+ query: data => ({
+ url: '/graphql',
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${btoa(`admin@gmail.com:admin`)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `
+ mutation {
+ banUser(
+ banReason : "${data.banReason}",
+ userId : ${data.userId})
+ }
+ `,
+ }),
+ }),
+ }),
+ }),
+})
+
+export const {
+ useGetUsersMutation,
+ useDeleteUserMutation,
+ useGetUserMutation,
+ useGetPostsByUserMutation,
+ useGetPaymentsLIstMutation,
+ useGetPaymentsByUserMutation,
+ useGetPostsListMutation,
+ useGetFollowersMutation,
+ useGetFollowingMutation,
+ useUnBanUserMutation,
+ useBanUserMutation,
+} = usersApi
diff --git a/src/entities/users/index.ts b/src/entities/users/index.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/pages/home/ui/Home.tsx b/src/pages/home/ui/Home.tsx
index 05781598..818c0461 100644
--- a/src/pages/home/ui/Home.tsx
+++ b/src/pages/home/ui/Home.tsx
@@ -1,14 +1,48 @@
-import Link from 'next/link'
+import React, { useEffect } from 'react'
-import { Button } from '@/shared/components'
+import { useSearchParams } from 'next/navigation'
+import { useRouter } from 'next/router'
+
+import s from './home.module.scss'
+
+import { useGetPostOfFollowersQuery } from '@/entities/posts/api/postsApi'
+import { PostsHome } from '@/pages/home/ui/PostsHome'
+import { Button, SwiperSlider } from '@/shared/components'
+import { useModal } from '@/shared/lib/hooks/open-or-close-hook'
+import { useAuth } from '@/shared/lib/hooks/useAuth'
import { getHeaderWithSidebarLayout } from '@/widgets/layouts'
+import { PostViewModal } from '@/widgets/postViewModal'
+import { PostCommentsView } from '@/widgets/postViewModal/UI/PostCommentsView'
function Home() {
+ const isSSR = useRouter().asPath.includes('home')
+ const { accessToken } = useAuth()
+ const { data: fakePost } = useGetPostOfFollowersQuery({ accessToken })
+
return (
-
-
- Profile Settings
-
+
+
+ {fakePost &&
+ fakePost.items.map((el: PostDataType) => {
+ return (
+ <>
+
+ >
+ )
+ })}
+
)
}
diff --git a/src/pages/home/ui/PostsHome.tsx b/src/pages/home/ui/PostsHome.tsx
new file mode 100644
index 00000000..679439c2
--- /dev/null
+++ b/src/pages/home/ui/PostsHome.tsx
@@ -0,0 +1,204 @@
+import React, { useEffect, useState } from 'react'
+
+import Image from 'next/image'
+import Link from 'next/link'
+import { useSearchParams } from 'next/navigation'
+
+import ThreeDotsWhite from '../../../../public/icons/thre-dots-white.png'
+
+import s from './postsHome.module.scss'
+
+import { useGetCommentQuery, useUpdateCommentMutation } from '@/entities/comments'
+import { useCreateAnswerMutation } from '@/entities/comments/api/commentsApi'
+import { BookmarkOutlineIcon, HeartOutline, TelegramIcon } from '@/shared/assets'
+import { CommentIcon } from '@/shared/assets/icons/CommentIcon'
+import PersonImg3 from '@/shared/assets/PersonImg3.png'
+import PersonImg4 from '@/shared/assets/PersonImg4.png'
+import { Button, SwiperSlider, TimeAgo, Typography } from '@/shared/components'
+import { AvatarSmallView } from '@/shared/components/avatarSmallView'
+import { useFormatDate, useTranslation } from '@/shared/lib'
+import { useModal } from '@/shared/lib/hooks/open-or-close-hook'
+import { useAuth } from '@/shared/lib/hooks/useAuth'
+import { PostViewModal } from '@/widgets/postViewModal'
+
+type Props = {
+ id?: number
+ ownerId: number
+ avatarOwner: string
+ userName: string
+ description: string
+ updatedAt: string
+ isSSR: boolean
+ img: any
+}
+export const PostsHome = ({
+ ownerId,
+ avatarOwner,
+ userName,
+ description,
+ updatedAt,
+ isSSR,
+ id,
+ img,
+}: Props) => {
+ const { t } = useTranslation()
+ const { formatDate } = useFormatDate(t.lg)
+ const [updateComments, { isLoading: isPostLoading }] = useUpdateCommentMutation()
+ const [createAnswer, { isLoading: isCreateAnswer }] = useCreateAnswerMutation()
+ const { accessToken } = useAuth()
+ const [comment, setComment] = useState
('')
+ const { data: dataAuth } = useGetCommentQuery({ postId: id, accessToken })
+ const { isAuth } = useAuth()
+ const [isAnswer, setIsAnswer] = useState(false)
+ const [commentId, setCommentId] = useState()
+
+ const { isOpen, openModal, closeModal, modalId } = useModal()
+ const searchParams = useSearchParams()
+ const postNumber = searchParams?.get('modalId') as string | undefined
+
+ useEffect(() => {
+ postNumber && openModal(+postNumber)
+ }, [postNumber])
+ const submitClickHandler = () => {
+ setComment('')
+ if (isAnswer) {
+ createAnswer({
+ content: comment,
+ commentId: commentId,
+ postId: id,
+ accessToken,
+ })
+ setIsAnswer(false)
+ } else
+ updateComments({
+ content: comment,
+ postId: id,
+ accessToken,
+ })
+ }
+
+ useEffect(() => {
+ if (isAnswer) return setComment('@' + userName + ',')
+ }, [isAnswer])
+
+ return (
+
+
+ {!!modalId && }
+
+
+
+
+
+
+ {userName}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/pages/home/ui/home.module.scss b/src/pages/home/ui/home.module.scss
new file mode 100644
index 00000000..ab668cb1
--- /dev/null
+++ b/src/pages/home/ui/home.module.scss
@@ -0,0 +1,14 @@
+.postCommentsView {
+ display: flex;
+ justify-content: flex-end;
+
+ width: 50rem;
+ min-height: 350px;
+ padding-top: 10px;
+}
+
+@media (width <= 576px) {
+ .postCommentsView {
+ justify-content: flex-start;
+ }
+}
diff --git a/src/pages/home/ui/postsHome.module.scss b/src/pages/home/ui/postsHome.module.scss
new file mode 100644
index 00000000..46f20c7f
--- /dev/null
+++ b/src/pages/home/ui/postsHome.module.scss
@@ -0,0 +1,286 @@
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ width: 30.5rem;
+ padding: 12px 24px;
+}
+
+.headerOnMiddle {
+ display: block;
+}
+
+.main {
+ max-width: 500px;
+ height: 321px;
+ word-break: break-all;
+}
+
+@media (width <= 576px) {
+ .main {
+ width: 24rem;
+ }
+}
+
+.postContent {
+ width: 100%;
+}
+
+.img {
+ min-width: 30.2rem;
+ min-height: 27.8rem;
+}
+
+@media (width <= 1024px) {
+ .main {
+ height: 180px;
+ }
+
+ .headerOnMiddle {
+ display: none;
+ }
+}
+
+.scrollContent {
+ width: 100%;
+}
+
+.footer {
+ width: 30.5rem;
+ padding-top: 11.3rem;
+}
+
+@media (width <= 576px) {
+ .main {
+ height: 190px;
+ }
+
+ .scrollContent {
+ background-color: transparent;
+ }
+
+ @media (width <= 576px) {
+ .buttonPublish {
+ width: 24rem;
+ }
+ }
+}
+
+.avatar {
+ display: flex;
+ column-gap: 12px;
+ align-items: center;
+}
+
+.smallAvatar {
+ border-radius: 50%;
+}
+
+.smallAvatarPost {
+ transform: translateY(5px);
+ margin-top: 10px;
+ border-radius: 50%;
+}
+
+.dots {
+ cursor: pointer;
+ color: var(--color-accent-500);
+}
+
+.content {
+ cursor: pointer;
+
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.dots1 {
+ width: 1.2rem;
+ height: 1.2rem;
+ margin-top: 0.5rem;
+ margin-right: 3.9rem;
+}
+
+.button {
+ margin: 0;
+ padding: 0;
+ color: var(--color-light-100);
+}
+
+.post {
+ display: flex;
+ column-gap: 12px;
+ align-items: flex-start;
+
+ width: 110%;
+ padding: 12px 24px;
+
+ overflow-wrap: break-word;
+}
+
+.updatedAt {
+ display: initial;
+ padding-left: 4px;
+ color: var(--color-light-900);
+}
+
+.allComment {
+ cursor: pointer;
+ padding-left: 1.5rem;
+ color: var(--color-light-900);
+}
+
+.InputField {
+ height: 1rem;
+
+ word-wrap: break-word;
+
+ background-color: transparent;
+ border: none;
+ outline: none;
+}
+
+.line {
+ position: relative;
+}
+
+.line::after {
+ content: '';
+
+ position: absolute;
+ bottom: 0;
+ left: 1.4rem;
+
+ width: 90%;
+ height: 2px;
+
+ background-color: var(--color-dark-100);
+ border-radius: 4px;
+}
+
+.answer {
+ overflow-x: hidden;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+
+ max-width: 480px;
+ margin-left: 3rem;
+}
+
+.answerNone {
+ display: none;
+}
+
+.buttonAnswer {
+ display: flex;
+ justify-content: space-evenly;
+
+ width: 14rem;
+ padding-bottom: 1px;
+
+ font-size: 0.8rem;
+ color: gray;
+
+ &:hover {
+ color: gainsboro;
+ }
+}
+
+.comment {
+ overflow-x: hidden;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+
+ max-width: 480px;
+}
+
+.like {
+ cursor: pointer;
+ padding: 24px 24px 0 0;
+}
+
+.share {
+ width: 30.5rem;
+ padding: 12px 24px;
+}
+
+.shareIcons {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.commentIcon {
+ cursor: pointer;
+}
+
+.shareIconsStart {
+ display: flex;
+ column-gap: 15px;
+ align-items: center;
+ margin-bottom: 9px;
+}
+
+.likeCounter {
+ display: flex;
+ column-gap: 12px;
+ align-items: center;
+
+ margin-top: 0.5rem;
+ padding: 0 24px 36px 0;
+}
+
+.descriptionPosts {
+ display: flex;
+ column-gap: 5px;
+ margin-top: 0.9rem;
+}
+
+.description {
+ color: #d5d4da;
+}
+
+.likeCounterNum {
+ transform: translateY(15px);
+}
+
+.avatarLayers {
+ position: relative;
+ width: 90px;
+}
+
+.smallAvatarLayer {
+ position: absolute;
+ border-radius: 50%;
+}
+
+.smallAvatarLayer:first-child {
+ z-index: 30;
+ left: 0;
+}
+
+.smallAvatarLayer:nth-child(2) {
+ z-index: 20;
+ left: 25px;
+}
+
+.smallAvatarLayer:nth-child(3) {
+ z-index: 10;
+ left: 50px;
+}
+
+.addComment {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ width: 30.5rem;
+ padding: 12px 24px;
+
+ font-size: 0.9rem;
+}
diff --git a/src/pages/moreInformation/index.ts b/src/pages/moreInformation/index.ts
new file mode 100644
index 00000000..799f9bc0
--- /dev/null
+++ b/src/pages/moreInformation/index.ts
@@ -0,0 +1 @@
+export { MoreInformationPage } from './ui/MoreInformationPage'
diff --git a/src/pages/moreInformation/ui/MoreInformationPage.tsx b/src/pages/moreInformation/ui/MoreInformationPage.tsx
new file mode 100644
index 00000000..028eba44
--- /dev/null
+++ b/src/pages/moreInformation/ui/MoreInformationPage.tsx
@@ -0,0 +1,14 @@
+import { getInfoUserLayout } from '@/widgets/layouts/superAdmin-layout/InfoUserLayout/InfoUserLayout'
+import MoreInformation from '@/widgets/superAdmin/userList/moreInformation/MoreInformation'
+
+const MoreInformationPage = () => {
+ return (
+
+
+
+ )
+}
+
+MoreInformationPage.getLayout = getInfoUserLayout
+
+export { MoreInformationPage }
diff --git a/src/pages/paymentsList/index.ts b/src/pages/paymentsList/index.ts
new file mode 100644
index 00000000..97e7de9f
--- /dev/null
+++ b/src/pages/paymentsList/index.ts
@@ -0,0 +1 @@
+export { PaymentsListPage } from './ui/PaymentsListPage'
diff --git a/src/pages/paymentsList/ui/PaymentsListPage.tsx b/src/pages/paymentsList/ui/PaymentsListPage.tsx
new file mode 100644
index 00000000..a63816fa
--- /dev/null
+++ b/src/pages/paymentsList/ui/PaymentsListPage.tsx
@@ -0,0 +1,14 @@
+import { getSuperAdminLayoutLayout } from '@/widgets/layouts/superAdmin-layout/SuperAdminLayout'
+import { PaymentsList } from '@/widgets/superAdmin/paymentsList/PaymentsList'
+
+const PaymentsListPage = () => {
+ return (
+
+ )
+}
+
+PaymentsListPage.getLayout = getSuperAdminLayoutLayout
+
+export { PaymentsListPage }
diff --git a/src/pages/postsList/index.ts b/src/pages/postsList/index.ts
new file mode 100644
index 00000000..5e3a885b
--- /dev/null
+++ b/src/pages/postsList/index.ts
@@ -0,0 +1 @@
+export { PostsListPage } from './ui/PostsListPage'
diff --git a/src/pages/postsList/ui/PostsListPage.tsx b/src/pages/postsList/ui/PostsListPage.tsx
new file mode 100644
index 00000000..05bce033
--- /dev/null
+++ b/src/pages/postsList/ui/PostsListPage.tsx
@@ -0,0 +1,19 @@
+import { ApolloProvider } from '@apollo/client'
+
+import { getSuperAdminLayoutLayout } from '@/widgets/layouts/superAdmin-layout/SuperAdminLayout'
+import client from '@/widgets/superAdmin/postsList/apolloClient/apolloClient'
+import { PostsList } from '@/widgets/superAdmin/postsList/PostsList'
+
+const PostsListPage = () => {
+ return (
+
+ )
+}
+
+PostsListPage.getLayout = getSuperAdminLayoutLayout
+
+export { PostsListPage }
diff --git a/src/pages/search/index.ts b/src/pages/search/index.ts
index b2bec761..88ff0d5b 100644
--- a/src/pages/search/index.ts
+++ b/src/pages/search/index.ts
@@ -1 +1 @@
-export { Search } from './ui/Search'
+export { SearchPage } from './ui/SearchPage'
diff --git a/src/pages/search/ui/Search.tsx b/src/pages/search/ui/Search.tsx
deleted file mode 100644
index d86808ce..00000000
--- a/src/pages/search/ui/Search.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { getHeaderWithSidebarLayout } from '@/widgets/layouts'
-
-function Search() {
- return Search
-}
-
-Search.getLayout = getHeaderWithSidebarLayout
-
-export { Search }
diff --git a/src/pages/search/ui/SearchPage.tsx b/src/pages/search/ui/SearchPage.tsx
new file mode 100644
index 00000000..fff21571
--- /dev/null
+++ b/src/pages/search/ui/SearchPage.tsx
@@ -0,0 +1,14 @@
+import { getHeaderWithSidebarLayout } from '@/widgets/layouts'
+import { SearchUser } from '@/widgets/search/ui/SearchUser'
+
+function SearchPage() {
+ return (
+
+
+
+ )
+}
+
+SearchPage.getLayout = getHeaderWithSidebarLayout
+
+export { SearchPage }
diff --git a/src/pages/superAdmin/index.ts b/src/pages/superAdmin/index.ts
new file mode 100644
index 00000000..7f56083b
--- /dev/null
+++ b/src/pages/superAdmin/index.ts
@@ -0,0 +1 @@
+export { AdminPage } from './ui/AdminPage'
diff --git a/src/pages/superAdmin/ui/AdminPage.tsx b/src/pages/superAdmin/ui/AdminPage.tsx
new file mode 100644
index 00000000..58893cce
--- /dev/null
+++ b/src/pages/superAdmin/ui/AdminPage.tsx
@@ -0,0 +1,14 @@
+import { getSuperAdminLayoutLayout } from '@/widgets/layouts/superAdmin-layout/SuperAdminLayout'
+import { Admin } from '@/widgets/superAdmin/superAdmin'
+
+const AdminPage = () => {
+ return (
+
+ )
+}
+
+AdminPage.getLayout = getSuperAdminLayoutLayout
+
+export { AdminPage }
diff --git a/src/pages/userList/index.ts b/src/pages/userList/index.ts
new file mode 100644
index 00000000..f48584b1
--- /dev/null
+++ b/src/pages/userList/index.ts
@@ -0,0 +1 @@
+export { UserListPage } from './ui/UserListPage'
diff --git a/src/pages/userList/ui/UserListPage.tsx b/src/pages/userList/ui/UserListPage.tsx
new file mode 100644
index 00000000..2190dbc3
--- /dev/null
+++ b/src/pages/userList/ui/UserListPage.tsx
@@ -0,0 +1,14 @@
+import { getSuperAdminLayoutLayout } from '@/widgets/layouts/superAdmin-layout/SuperAdminLayout'
+import { UserList } from '@/widgets/superAdmin'
+
+const UserListPage = () => {
+ return (
+
+
+
+ )
+}
+
+UserListPage.getLayout = getSuperAdminLayoutLayout
+
+export { UserListPage }
diff --git a/src/pages/userProfilePage/index.ts b/src/pages/userProfilePage/index.ts
new file mode 100644
index 00000000..7777f69d
--- /dev/null
+++ b/src/pages/userProfilePage/index.ts
@@ -0,0 +1 @@
+export { UserProfilePage } from './ui/UserProfilePage'
diff --git a/src/pages/userProfilePage/ui/UserProfilePage.tsx b/src/pages/userProfilePage/ui/UserProfilePage.tsx
new file mode 100644
index 00000000..72868f2d
--- /dev/null
+++ b/src/pages/userProfilePage/ui/UserProfilePage.tsx
@@ -0,0 +1,19 @@
+import { useRouter } from 'next/router'
+
+import { getHeaderWithSidebarLayout } from '@/widgets/layouts'
+import { UserProfile } from '@/widgets/search/ui/userProfile/UserProfile'
+
+function UserProfilePage() {
+ const router = useRouter()
+ const userName = router.query
+
+ return (
+
+
+
+ )
+}
+
+UserProfilePage.getLayout = getHeaderWithSidebarLayout
+
+export { UserProfilePage }
diff --git a/src/shared/assets/_mixins.scss b/src/shared/assets/_mixins.scss
index ae52e669..0f1d754f 100644
--- a/src/shared/assets/_mixins.scss
+++ b/src/shared/assets/_mixins.scss
@@ -10,3 +10,37 @@
background-color: var(--color-dark-500);
border-radius: 2px;
}
+
+@mixin table_styles() {
+ width: 100%;
+ border: 1px solid var(--color-dark-500);
+
+ th {
+ background-color: var(--color-dark-500);
+ }
+
+ th,
+ td {
+ padding: 20px 30px;
+ text-align: left;
+ border-bottom: 1px solid var(--color-dark-500);
+ }
+}
+
+@mixin table_photo() {
+ border-collapse: separate;
+}
+
+.table td {
+ padding: 5px;
+ border: 1px solid black;
+}
+
+.table tr td:first-child {
+ padding-left: 0;
+}
+
+.table tr td:last-child {
+ padding-right: 0;
+}
+
diff --git a/src/shared/assets/icons/ArrowBack.tsx b/src/shared/assets/icons/ArrowBack.tsx
new file mode 100644
index 00000000..db4d1792
--- /dev/null
+++ b/src/shared/assets/icons/ArrowBack.tsx
@@ -0,0 +1,17 @@
+export const ArrowBack = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/shared/assets/icons/BlockIcon.tsx b/src/shared/assets/icons/BlockIcon.tsx
new file mode 100644
index 00000000..4c4a7967
--- /dev/null
+++ b/src/shared/assets/icons/BlockIcon.tsx
@@ -0,0 +1,28 @@
+import { Ref, SVGProps, forwardRef } from 'react'
+const BlockIcon = (props: SVGProps, ref: Ref) => (
+
+
+
+
+
+
+
+
+
+
+
+)
+const ForwardRef = forwardRef(BlockIcon)
+
+export { ForwardRef as BlockIcon }
diff --git a/src/shared/assets/icons/Cards.tsx b/src/shared/assets/icons/Cards.tsx
new file mode 100644
index 00000000..e47493f7
--- /dev/null
+++ b/src/shared/assets/icons/Cards.tsx
@@ -0,0 +1,31 @@
+import { forwardRef, memo, Ref, SVGProps } from 'react'
+
+const SvgComponent = (props: SVGProps, ref: Ref) => (
+
+
+
+
+ {/* fill={props.fill ? props.fill : 'white'} */}
+ {/* /> */}
+
+)
+const ForwardRef = forwardRef(SvgComponent)
+
+export default memo(ForwardRef)
diff --git a/src/shared/assets/icons/ChromeIcon.tsx b/src/shared/assets/icons/ChromeIcon.tsx
new file mode 100644
index 00000000..6eb7b018
--- /dev/null
+++ b/src/shared/assets/icons/ChromeIcon.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react'
+
+export const ChromeIcon = ({ size = 30, color = '#fff', ...rest }) => {
+ return (
+
+
+
+ )
+}
diff --git a/src/shared/assets/icons/CommentIcon.tsx b/src/shared/assets/icons/CommentIcon.tsx
new file mode 100644
index 00000000..1473c3d4
--- /dev/null
+++ b/src/shared/assets/icons/CommentIcon.tsx
@@ -0,0 +1,16 @@
+export const CommentIcon = () => {
+ return (
+
+
+
+ )
+}
diff --git a/src/shared/assets/icons/DeleteUserIcon.tsx b/src/shared/assets/icons/DeleteUserIcon.tsx
new file mode 100644
index 00000000..ff8449f4
--- /dev/null
+++ b/src/shared/assets/icons/DeleteUserIcon.tsx
@@ -0,0 +1,35 @@
+import { Ref, SVGProps, forwardRef } from 'react'
+const DeleteUserIcon = (props: SVGProps, ref: Ref) => (
+
+
+
+
+
+
+
+
+
+
+
+
+)
+const ForwardRef = forwardRef(DeleteUserIcon)
+
+export { ForwardRef as DeleteUserIcon }
diff --git a/src/shared/assets/icons/EllipsisIcon.tsx b/src/shared/assets/icons/EllipsisIcon.tsx
new file mode 100644
index 00000000..e988ba56
--- /dev/null
+++ b/src/shared/assets/icons/EllipsisIcon.tsx
@@ -0,0 +1,31 @@
+import { Ref, SVGProps, forwardRef } from 'react'
+const EllipsisIcon = (
+ props: SVGProps & { 'data-state'?: string },
+ ref: Ref
+) => (
+
+
+
+
+
+)
+const ForwardRef = forwardRef(EllipsisIcon)
+
+export { ForwardRef as EllipsisIcon }
diff --git a/src/shared/assets/icons/Filter.tsx b/src/shared/assets/icons/Filter.tsx
new file mode 100644
index 00000000..77b7bd04
--- /dev/null
+++ b/src/shared/assets/icons/Filter.tsx
@@ -0,0 +1,17 @@
+import { Ref, SVGProps } from 'react'
+
+export const Filter = (props: SVGProps) => {
+ return (
+
+
+
+
+ )
+}
diff --git a/src/shared/assets/icons/MackIcon.tsx b/src/shared/assets/icons/MackIcon.tsx
new file mode 100644
index 00000000..7f393b94
--- /dev/null
+++ b/src/shared/assets/icons/MackIcon.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react'
+
+export const MackIcon = ({ size = 30, color = '#fff', ...rest }) => {
+ return (
+
+
+
+ )
+}
diff --git a/src/shared/assets/icons/PayPal.tsx b/src/shared/assets/icons/PayPal.tsx
new file mode 100644
index 00000000..5a18848e
--- /dev/null
+++ b/src/shared/assets/icons/PayPal.tsx
@@ -0,0 +1,75 @@
+import * as React from 'react'
+
+export function PayPal(props: React.SVGProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/shared/assets/icons/PhoneIcon.tsx b/src/shared/assets/icons/PhoneIcon.tsx
new file mode 100644
index 00000000..568bca66
--- /dev/null
+++ b/src/shared/assets/icons/PhoneIcon.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react'
+
+export const PhoneIcon = ({ size = 30, color = '#fff', ...rest }) => {
+ return (
+
+
+
+ )
+}
diff --git a/src/shared/assets/icons/Polygon.tsx b/src/shared/assets/icons/Polygon.tsx
new file mode 100644
index 00000000..127f49c2
--- /dev/null
+++ b/src/shared/assets/icons/Polygon.tsx
@@ -0,0 +1,7 @@
+export const Polygon = () => {
+ return (
+
+
+
+ )
+}
diff --git a/src/shared/assets/icons/PolygonUp.tsx b/src/shared/assets/icons/PolygonUp.tsx
new file mode 100644
index 00000000..221a3e4b
--- /dev/null
+++ b/src/shared/assets/icons/PolygonUp.tsx
@@ -0,0 +1,7 @@
+export const PolygonUp = () => {
+ return (
+
+
+
+ )
+}
diff --git a/src/shared/assets/icons/Post.tsx b/src/shared/assets/icons/Post.tsx
new file mode 100644
index 00000000..0b9b45e6
--- /dev/null
+++ b/src/shared/assets/icons/Post.tsx
@@ -0,0 +1,32 @@
+import { forwardRef, memo, Ref, SVGProps } from 'react'
+
+const SvgComponent = (props: SVGProps, ref: Ref) => (
+
+
+
+
+
+
+
+
+
+
+
+)
+const ForwardRef = forwardRef(SvgComponent)
+
+export default memo(ForwardRef)
diff --git a/src/shared/assets/icons/Stripe.tsx b/src/shared/assets/icons/Stripe.tsx
new file mode 100644
index 00000000..10e6081e
--- /dev/null
+++ b/src/shared/assets/icons/Stripe.tsx
@@ -0,0 +1,33 @@
+import * as React from 'react'
+
+export function Stripe(props: React.SVGProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/shared/assets/icons/comment.svg b/src/shared/assets/icons/comment.svg
new file mode 100644
index 00000000..87b8b0ad
--- /dev/null
+++ b/src/shared/assets/icons/comment.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/shared/assets/icons/index.ts b/src/shared/assets/icons/index.ts
index fdb8db39..75a51b9c 100644
--- a/src/shared/assets/icons/index.ts
+++ b/src/shared/assets/icons/index.ts
@@ -24,4 +24,6 @@ export * from './HeartOutline'
export * from './HeartRed'
export * from './TelegramIcon'
export * from './BookmarkOutline'
+export * from './PayPal'
+export * from './Stripe'
export { IconAdd } from './IconAdd'
diff --git a/src/shared/assets/index.ts b/src/shared/assets/index.ts
index 913d01b7..d2af9f1f 100644
--- a/src/shared/assets/index.ts
+++ b/src/shared/assets/index.ts
@@ -1,5 +1,6 @@
import exp from 'constants'
-
+export { default as PostIcon } from './icons/Post'
+export { default as CardsIcon } from './icons/Cards'
export { default as GithubIcon } from './icons/GitHubIcon.svg'
export { default as GoogleIcon } from './icons/GoogleIcon.svg'
export { default as EyeOutlineIcon } from './icons/Eye-outlineIcon.svg'
diff --git a/src/shared/components/avatarSmallView/index.tsx b/src/shared/components/avatarSmallView/index.tsx
index b72ecf95..0f0cdf0a 100644
--- a/src/shared/components/avatarSmallView/index.tsx
+++ b/src/shared/components/avatarSmallView/index.tsx
@@ -8,15 +8,22 @@ import SmileImg from '@/shared/assets/SmileImg.png'
import { cn } from '@/shared/lib/utils'
export type AvatarProps = {
+ width?: number
+ height?: number
avatarOwner?: string
} & ComponentPropsWithoutRef
-export const AvatarSmallView = ({ avatarOwner, className }: AvatarProps) => {
+export const AvatarSmallView = ({
+ avatarOwner,
+ width = 36,
+ height = 36,
+ className,
+}: AvatarProps) => {
return (
diff --git a/src/shared/components/button/button.tsx b/src/shared/components/button/button.tsx
index 508c9fc3..f8fec75c 100644
--- a/src/shared/components/button/button.tsx
+++ b/src/shared/components/button/button.tsx
@@ -6,22 +6,13 @@ export type ButtonProps = {
variant?: 'primary' | 'secondary' | 'outline' | 'link'
fullWidth?: boolean
as?: T
- onClick?: () => void
} & ComponentPropsWithoutRef
export const Button = (props: ButtonProps) => {
- const {
- variant = 'primary',
- fullWidth,
- className,
- onClick,
- as: Component = 'button',
- ...rest
- } = props
+ const { variant = 'primary', fullWidth, className, as: Component = 'button', ...rest } = props
return (
{
if (date && date instanceof Date) {
setDateValue(format(date, 'yyyy-MM-dd'))
+
onBlur(date)
}
}
diff --git a/src/shared/components/dropdown/dropdown.module.scss b/src/shared/components/dropdown/dropdown.module.scss
index c094737c..7107c101 100644
--- a/src/shared/components/dropdown/dropdown.module.scss
+++ b/src/shared/components/dropdown/dropdown.module.scss
@@ -30,6 +30,7 @@
z-index: 0;
top: -3px;
right: -1px;
+
// transform: rotate(45deg);
width: 7px;
diff --git a/src/shared/components/dropdown/dropdown.tsx b/src/shared/components/dropdown/dropdown.tsx
index b2b5584b..eaab94fa 100644
--- a/src/shared/components/dropdown/dropdown.tsx
+++ b/src/shared/components/dropdown/dropdown.tsx
@@ -10,7 +10,7 @@ import {
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { clsx } from 'clsx'
-import { Typography } from '../typography'
+import { options, Typography } from '../typography'
import s from './dropdown.module.scss'
@@ -109,11 +109,13 @@ export const CustomDropdownItem = ({
type DropdownItemWithIconProps = Omit & {
title: string
+ variant?: (typeof options)[number]
icon?: ReactNode
} & ComponentPropsWithoutRef
export const CustomDropdownItemWithIcon = ({
title,
+ variant,
icon,
onSelect,
disabled,
@@ -136,7 +138,7 @@ export const CustomDropdownItemWithIcon = ({
{...rest}
>
{icon}
- {title}
+ {title}
)
}
diff --git a/src/shared/components/imageCard/ui/imageCard.module.scss b/src/shared/components/imageCard/ui/imageCard.module.scss
index 6e7da7c1..c6f74b3d 100644
--- a/src/shared/components/imageCard/ui/imageCard.module.scss
+++ b/src/shared/components/imageCard/ui/imageCard.module.scss
@@ -1,6 +1,8 @@
.image {
cursor: pointer;
+ position: relative;
+
overflow: hidden;
display: flex;
align-items: center;
diff --git a/src/shared/components/imageCard/ui/imageCard.tsx b/src/shared/components/imageCard/ui/imageCard.tsx
index a365ff97..59422299 100644
--- a/src/shared/components/imageCard/ui/imageCard.tsx
+++ b/src/shared/components/imageCard/ui/imageCard.tsx
@@ -13,7 +13,7 @@ type Props = ImageProps & {
openModal?: (postId: number) => void
}
-export const ImageCard = ({ postId, src, alt, cardClassName, width, height, openModal }: Props) => {
+export const ImageCard = ({ postId, src, alt, cardClassName, openModal }: Props) => {
const [loading, setLoading] = useState(true)
useFetchLoader(loading)
@@ -32,10 +32,12 @@ export const ImageCard = ({ postId, src, alt, cardClassName, width, height, open
>
setLoading(false)}
/>
diff --git a/src/shared/components/notificatification-bell/NotificationBell.tsx b/src/shared/components/notificatification-bell/NotificationBell.tsx
index 5daa6667..34655295 100644
--- a/src/shared/components/notificatification-bell/NotificationBell.tsx
+++ b/src/shared/components/notificatification-bell/NotificationBell.tsx
@@ -1,4 +1,4 @@
-import { FC, useState } from 'react'
+import React from 'react'
import { clsx } from 'clsx'
@@ -13,9 +13,10 @@ export type NotificationProps = {
className?: string
toggle: boolean
setToggle: React.Dispatch>
+ count: number
}
-export const NotificationBell: FC = ({ toggle, setToggle, className }) => {
+export const NotificationBell = ({ toggle, setToggle, className, count }: NotificationProps) => {
const classNames = {
notificationBlock: clsx(s.notificationBlock, className),
}
@@ -25,7 +26,7 @@ export const NotificationBell: FC = ({ toggle, setToggle, cla
{toggle ? : }
{!toggle && (
- 2
+ {count}
)}
diff --git a/src/shared/components/notification-item/NotificationItem.tsx b/src/shared/components/notification-item/NotificationItem.tsx
index 65ada761..4c103d85 100644
--- a/src/shared/components/notification-item/NotificationItem.tsx
+++ b/src/shared/components/notification-item/NotificationItem.tsx
@@ -1,25 +1,37 @@
-import React, { FC } from 'react'
-
-import { Typography } from '..'
+import React from 'react'
import s from './NotificationItem.module.scss'
-export const NotificationItem: FC = () => {
+import { Typography } from '@/shared/components'
+import { useTranslation } from '@/shared/lib'
+
+type Props = {
+ message: string
+ newMessage?: boolean
+ notifyAt: Date
+}
+export const NotificationItem = ({ message, newMessage, notifyAt, ...restProps }: Props) => {
+ const { t } = useTranslation()
+
return (
-
+
- Новое уведомление!
-
-
- Новое
+ {t.new_notification}
+ {newMessage && (
+
+ {t.new_title}
+
+ )}
- Следующий платеж у вас спишется через 1 день
+ {t.notification(message)}
- 1 день назад
+ {new Date(notifyAt).setHours(0, 0, 0, 0) === new Date().setHours(0, 0, 0, 0)
+ ? t.today
+ : new Date(notifyAt).toLocaleDateString('ru-RU')}
)
diff --git a/src/shared/components/pagination/pagination.module.scss b/src/shared/components/pagination/pagination.module.scss
index 7e1e161b..1e450f9d 100644
--- a/src/shared/components/pagination/pagination.module.scss
+++ b/src/shared/components/pagination/pagination.module.scss
@@ -74,6 +74,7 @@
display: flex;
gap: 5px;
+ align-items: center;
margin-top: 15px;
margin-bottom: 15px;
@@ -84,4 +85,6 @@
gap: 10px;
align-items: center;
justify-content: center;
+
+ white-space: nowrap;
}
diff --git a/src/shared/components/pagination/pagination.tsx b/src/shared/components/pagination/pagination.tsx
index 8b92ea5a..321a97c6 100644
--- a/src/shared/components/pagination/pagination.tsx
+++ b/src/shared/components/pagination/pagination.tsx
@@ -6,6 +6,8 @@ import { OptionsType, SelectCustom } from '../select'
import s from './pagination.module.scss'
+import { useTranslation } from '@/shared/lib'
+
export type PaginationProps = {
totalCount: number | undefined
currentPage: number
@@ -68,6 +70,7 @@ export const Pagination = (props: PaginationProps) => {
options,
portionValue,
} = props
+ const { t } = useTranslation()
const pagesCount = Math.ceil(totalCount / pageSize)
@@ -129,7 +132,7 @@ export const Pagination = (props: PaginationProps) => {
- Show
+ {t.show}
{
defaultValue={pageSize.toString()}
onValueChange={onPageSizeHandler}
/>
- on page
+ {t.on_page}
)
diff --git a/src/shared/components/public-post-card/ui/PublicPostCard.tsx b/src/shared/components/public-post-card/ui/PublicPostCard.tsx
index 77da9ef1..e039e523 100644
--- a/src/shared/components/public-post-card/ui/PublicPostCard.tsx
+++ b/src/shared/components/public-post-card/ui/PublicPostCard.tsx
@@ -59,10 +59,12 @@ export const PublicPostCard: FC = ({
}}
>
{imagesUrl?.map((image: any, index: number) => {
+ if (image.width !== 1440) return null
+
return (
+ tags: ['autodocs'],
+} satisfies Meta
-export const Default = {
- // @ts-ignore
- render: args => {
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ render: () => {
return (
Header
diff --git a/src/shared/components/sidebar/Sidebar.tsx b/src/shared/components/sidebar/Sidebar.tsx
index be1cc017..a7ed050c 100644
--- a/src/shared/components/sidebar/Sidebar.tsx
+++ b/src/shared/components/sidebar/Sidebar.tsx
@@ -20,8 +20,10 @@ import {
import s from './Sidebar.module.scss'
-import { useTranslation } from '@/shared/lib'
+import { addNewPhoto } from '@/app/services/cropper-slice'
+import { useAppDispatch, useTranslation } from '@/shared/lib'
import { useModal } from '@/shared/lib/hooks/open-or-close-hook'
+import useIndexedDB from '@/shared/lib/hooks/useIndexedDB'
import { AddPostModal } from '@/widgets/addPostModal/AddPostModal'
import { LogOutButton } from '@/widgets/logOut'
@@ -29,11 +31,22 @@ export const Sidebar = () => {
const router = useRouter()
const { t } = useTranslation()
const { isOpen, openModal, closeModal } = useModal()
+ const { getAllPhotos } = useIndexedDB('photoGalleryDB', { photoStore: 'photos' })
+ const dispatch = useAppDispatch()
+
const handleOpenMyProfileAndAddPost = () => {
router.push('/my-profile')
openModal()
}
+ getAllPhotos(photos => {
+ photos.forEach(item => {
+ if (item) {
+ dispatch(addNewPhoto(item.imageUrl))
+ }
+ })
+ })
+
return (
diff --git a/src/shared/components/sidebarAdmin/SidebarAdmin.module.scss b/src/shared/components/sidebarAdmin/SidebarAdmin.module.scss
new file mode 100644
index 00000000..375c50ea
--- /dev/null
+++ b/src/shared/components/sidebarAdmin/SidebarAdmin.module.scss
@@ -0,0 +1,83 @@
+.box {
+ z-index: auto;
+
+ display: inline-flex;
+ justify-content: space-between;
+
+ width: 220px;
+ height: calc(100vh - 4rem);
+ padding: 10px 60px;
+
+ color: var(--color-text-primary);
+
+ background-color: var(--color-dark-700);
+ border-right: 1px solid var(--color-dark-300);
+}
+
+.contentBox {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+
+ li {
+ cursor: pointer;
+ list-style: none;
+ }
+}
+
+.content {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ justify-content: flex-start;
+
+ margin-bottom: 24px;
+
+ font-size: 14px;
+ font-weight: 500;
+ font-style: normal;
+ line-height: 24px;
+ color: #fff;
+ text-decoration: none;
+
+ &:active {
+ color: var(--color-accent-500);
+ fill: var(--color-accent-500);
+ stroke: var(--color-accent-500);
+ }
+
+ &:hover {
+ color: var(--color-accent-100);
+ stroke: var(--color-accent-100);
+ }
+
+ &:focus-visible {
+ border-radius: 2px;
+ outline: 2px solid var(--color-accent-700);
+ }
+
+ &:disabled {
+ color: var(--color-dark-100);
+ stroke: var(--color-dark-100);
+ }
+}
+
+.marginTop {
+ flex: 0.3 3 10px;
+}
+
+.marginBox {
+ flex: 1 3 10px;
+}
+
+.largeMargin {
+ flex: 3 1 10px;
+}
+
+.activeLink {
+ font-size: 14px;
+ font-weight: 700;
+ font-style: normal;
+ line-height: 24px;
+ color: var(--color-accent-500);
+}
diff --git a/src/shared/components/sidebarAdmin/SidebarAdmin.stories.tsx b/src/shared/components/sidebarAdmin/SidebarAdmin.stories.tsx
new file mode 100644
index 00000000..dadd592c
--- /dev/null
+++ b/src/shared/components/sidebarAdmin/SidebarAdmin.stories.tsx
@@ -0,0 +1,22 @@
+import { Meta } from '@storybook/react'
+
+import { SidebarAdmin } from './SidebarAdmin'
+
+export default {
+ title: 'Components/SidebarAdmin',
+ component: SidebarAdmin,
+ tags: ['autodocs'],
+} satisfies Meta
+
+export const Default = {
+ render: () => {
+ return (
+
+ )
+ },
+}
diff --git a/src/shared/components/sidebarAdmin/SidebarAdmin.tsx b/src/shared/components/sidebarAdmin/SidebarAdmin.tsx
new file mode 100644
index 00000000..26411463
--- /dev/null
+++ b/src/shared/components/sidebarAdmin/SidebarAdmin.tsx
@@ -0,0 +1,83 @@
+import { useEffect } from 'react'
+
+import { clsx } from 'clsx'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+import {
+ CardsIcon,
+ IconUser,
+ IconUser2,
+ LogOutIcon,
+ MessangersIcon,
+ PostIcon,
+ StatisticsIcon,
+} from '../../assets'
+
+import s from './SidebarAdmin.module.scss'
+
+import { useTranslation } from '@/shared/lib'
+import { LogOutButton } from '@/widgets/logOut'
+
+export const SidebarAdmin = () => {
+ const router = useRouter()
+ const { t } = useTranslation()
+
+ const onClickHandler = () => {
+ localStorage.removeItem('isAdmin')
+ }
+
+ return (
+
+
+
+
+
+
+ {router.pathname == '/userList' ? : }
+
+ {t.sidebarAdmin.userList}
+
+
+
+
+
+ {t.sidebarAdmin.statistics}
+
+
+
+
+ {router.pathname === '/paymentsList' ? : }
+
+ {t.sidebarAdmin.paymentsList}
+
+
+
+
+
+ {router.pathname === '/postsList' ? : }
+
+ {t.sidebarAdmin.postsList}
+
+
+
+
+
+
+
+
+
+
+ {t.sidebar.log_out}
+
+
+
+
+
+ )
+}
diff --git a/src/shared/components/sidebarAdmin/index.ts b/src/shared/components/sidebarAdmin/index.ts
new file mode 100644
index 00000000..c755cddf
--- /dev/null
+++ b/src/shared/components/sidebarAdmin/index.ts
@@ -0,0 +1 @@
+export * from './SidebarAdmin'
diff --git a/src/shared/components/swiperSlider/SwiperSlider.tsx b/src/shared/components/swiperSlider/SwiperSlider.tsx
index c6cdfd2f..1fd924ff 100644
--- a/src/shared/components/swiperSlider/SwiperSlider.tsx
+++ b/src/shared/components/swiperSlider/SwiperSlider.tsx
@@ -2,6 +2,7 @@ import Image from 'next/image'
import { Navigation, Pagination, Scrollbar } from 'swiper/modules'
import { Swiper, SwiperSlide } from 'swiper/react'
+import s from '../../../pages/home/ui/postsHome.module.scss'
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
@@ -10,9 +11,10 @@ import './swiper-slider.scss'
type Props = {
imagesUrl: ImagesUrlData[]
+ postsHome: boolean
}
-export const SwiperSlider = ({ imagesUrl }: Props) => {
+export const SwiperSlider = ({ imagesUrl, postsHome }: Props) => {
return (
{
style={{ height: '100%', width: '100%' }}
>
{imagesUrl?.map((image: any, index: number) => {
+ if (image.width !== 1440) return null
+
return (
-
-
+
+
)
})}
diff --git a/src/shared/constants/enum.ts b/src/shared/constants/enum.ts
new file mode 100644
index 00000000..ffb596e8
--- /dev/null
+++ b/src/shared/constants/enum.ts
@@ -0,0 +1,10 @@
+export enum SortDirection {
+ DESC = 'desc',
+ ASC = 'asc',
+}
+
+export enum UserBlockStatus {
+ ALL = 'ALL',
+ BLOCKED = 'BLOCKED',
+ UNBLOCKED = 'UNBLOCKED',
+}
diff --git a/src/shared/constants/ext-urls.ts b/src/shared/constants/ext-urls.ts
index 18b9575c..c8e33bee 100644
--- a/src/shared/constants/ext-urls.ts
+++ b/src/shared/constants/ext-urls.ts
@@ -2,9 +2,9 @@ const CLIENT_ID = process.env.google_client_id
export const LOCAL_URL = 'http://localhost:3000'
-export const BASE_URL = 'https://incta.online'
+export const BASE_URL = 'https://mypicto.ru'
-export const BASE_URL_INCTA = 'https://incta.online'
+export const BASE_URL_INCTA = 'https://mypicto.ru'
export const BASE_WORK_URL = 'https://inctagram.work/api/v1'
diff --git a/src/shared/constants/tab.ts b/src/shared/constants/tab.ts
new file mode 100644
index 00000000..2c0fec35
--- /dev/null
+++ b/src/shared/constants/tab.ts
@@ -0,0 +1,8 @@
+export const tabType: { [key: string]: string } = {
+ DAY: '1 day',
+ WEEKLY: '7 days',
+ MONTHLY: '1 month',
+ STRIPE: 'Stripe',
+ PAYPAL: 'PayPal',
+ CREDIT_CARD: 'Credit Card',
+}
diff --git a/src/shared/lib/hooks/useAdmin.ts b/src/shared/lib/hooks/useAdmin.ts
new file mode 100644
index 00000000..22692d88
--- /dev/null
+++ b/src/shared/lib/hooks/useAdmin.ts
@@ -0,0 +1,11 @@
+import { useAppSelector } from './index'
+
+import { selectIsAdmin } from '@/app/services/admin-slice'
+
+export const useAdmin = () => {
+ const isAdmin = useAppSelector(selectIsAdmin)
+
+ return {
+ isAdmin,
+ }
+}
diff --git a/src/shared/lib/hooks/useIndexedDB.ts b/src/shared/lib/hooks/useIndexedDB.ts
new file mode 100644
index 00000000..97fb8387
--- /dev/null
+++ b/src/shared/lib/hooks/useIndexedDB.ts
@@ -0,0 +1,223 @@
+import { useEffect, useState } from 'react'
+
+interface Photo {
+ id: number
+ imageUrl: string
+}
+
+interface Notification {
+ id: number
+ message: string
+ notifyAt: Date
+}
+
+interface UseIndexedDBProps {
+ addPhoto: (imageUrl: string) => void
+ addNotification: (notifications: MessagesNotif, callback: () => void) => void
+ getPhoto: (id: number, callback: (photo: string | undefined) => void) => void
+ getNotification: (id: number, callback: (notification: Notification | undefined) => void) => void
+ deletePhotos: () => void
+ deleteNotifications: (keys: (IDBValidKey | IDBKeyRange)[]) => void
+ getAllPhotos: (callback: (photos: Photo[]) => void) => void
+ getAllNotifications: (callback: (notifications: MessagesNotif[]) => void) => void
+ isAddedPhoto: boolean
+}
+
+const useIndexedDB = (
+ dbName: string,
+ storeNames: { photoStore?: string; notificationStore?: string },
+ dbVersion: number = 1
+): UseIndexedDBProps => {
+ const [db, setDb] = useState(null)
+ const [isAddedPhoto, setIsAddedPhoto] = useState(true)
+ const [isDbReady, setIsDbReady] = useState(false)
+
+ useEffect(() => {
+ const request = indexedDB.open(dbName, dbVersion)
+
+ request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
+ const db = request.result
+
+ if (!db.objectStoreNames.contains(storeNames.photoStore as string)) {
+ db.createObjectStore(storeNames.photoStore as string, {
+ keyPath: 'id',
+ autoIncrement: true,
+ })
+ }
+
+ if (!db.objectStoreNames.contains(storeNames.notificationStore as string)) {
+ db.createObjectStore(storeNames.notificationStore as string, {
+ keyPath: 'id',
+ autoIncrement: true,
+ })
+ }
+ }
+
+ request.onsuccess = (event: Event) => {
+ setDb((event.target as IDBOpenDBRequest).result)
+ setIsDbReady(true)
+ }
+
+ request.onerror = (event: Event) => {
+ console.error('Database error:', (event.target as IDBOpenDBRequest).error)
+ }
+ }, [])
+
+ const addPhoto = (imageUrl: string) => {
+ if (!db || !storeNames.photoStore) return
+
+ const transaction = db.transaction([storeNames.photoStore], 'readwrite')
+ const objectStore = transaction.objectStore(storeNames.photoStore)
+
+ const request = objectStore.add({ imageUrl })
+
+ request.onsuccess = () => {
+ setIsAddedPhoto(true)
+ }
+
+ request.onerror = (event: Event) => {
+ console.error('Error adding photo:', (event.target as IDBRequest).error)
+ }
+ }
+
+ const addNotification = (notification: MessagesNotif, callback: () => void) => {
+ if (!isDbReady || !db || !storeNames.notificationStore) return
+
+ const transaction = db.transaction([storeNames.notificationStore], 'readwrite')
+ const objectStore = transaction.objectStore(storeNames.notificationStore)
+
+ const request = objectStore.add(notification)
+
+ request.onsuccess = () => {
+ callback()
+ }
+
+ request.onerror = (event: Event) => {
+ console.error('Error adding notification:', (event.target as IDBRequest).error)
+ }
+ }
+
+ const getPhoto = (id: number, callback: (photo: string | undefined) => void) => {
+ if (!isDbReady || !db || !storeNames.photoStore) return
+
+ const transaction = db.transaction([storeNames.photoStore], 'readonly')
+ const objectStore = transaction.objectStore(storeNames.photoStore)
+
+ const request = objectStore.get(id)
+
+ request.onsuccess = (event: Event) => {
+ callback((event.target as IDBRequest).result)
+ }
+
+ request.onerror = (event: Event) => {
+ console.error('Error retrieving photo:', (event.target as IDBRequest).error)
+ }
+ }
+
+ const getNotification = (
+ id: number,
+ callback: (notification: Notification | undefined) => void
+ ) => {
+ if (!isDbReady || !db || !storeNames.notificationStore) return
+
+ const transaction = db.transaction([storeNames.notificationStore], 'readonly')
+ const objectStore = transaction.objectStore(storeNames.notificationStore)
+
+ const request = objectStore.get(id)
+
+ request.onsuccess = (event: Event) => {
+ callback((event.target as IDBRequest).result)
+ }
+
+ request.onerror = (event: Event) => {
+ console.error('Error retrieving notification:', (event.target as IDBRequest).error)
+ }
+ }
+
+ const deletePhotos = () => {
+ if (!isDbReady || !db || !storeNames.photoStore) return
+
+ const transaction = db.transaction([storeNames.photoStore], 'readwrite')
+ const objectStore = transaction.objectStore(storeNames.photoStore)
+
+ const request = objectStore.clear()
+
+ request.onsuccess = () => {
+ setIsAddedPhoto(false)
+ }
+
+ request.onerror = (event: Event) => {
+ console.error('Error deleting photos:', (event.target as IDBRequest).error)
+ }
+ }
+
+ const deleteNotifications = (keys: (IDBValidKey | IDBKeyRange)[]) => {
+ if (!isDbReady || !db || !storeNames.notificationStore) return
+
+ const transaction = db.transaction([storeNames.notificationStore], 'readwrite')
+ const objectStore = transaction.objectStore(storeNames.notificationStore)
+
+ keys.forEach(key => {
+ const request = objectStore.delete(key)
+
+ request.onerror = (event: Event) => {
+ console.error(
+ `Error deleting notification with key ${key}:`,
+ (event.target as IDBRequest).error
+ )
+ }
+ })
+
+ transaction.onerror = (event: Event) => {
+ console.error('Transaction error:', (event.target as IDBTransaction).error)
+ }
+ }
+
+ const getAllPhotos = (callback: (photos: Photo[]) => void) => {
+ if (!isDbReady || !db || !storeNames.photoStore) return
+
+ const transaction = db.transaction([storeNames.photoStore], 'readonly')
+ const objectStore = transaction.objectStore(storeNames.photoStore)
+
+ const request = objectStore.getAll()
+
+ request.onsuccess = (event: Event) => {
+ callback((event.target as IDBRequest).result)
+ }
+
+ request.onerror = (event: Event) => {
+ console.error('Error retrieving all photos:', (event.target as IDBRequest).error)
+ }
+ }
+
+ const getAllNotifications = (callback: (notifications: MessagesNotif[]) => void) => {
+ if (!isDbReady || !db || !storeNames.notificationStore) return
+
+ const transaction = db.transaction([storeNames.notificationStore], 'readonly')
+ const objectStore = transaction.objectStore(storeNames.notificationStore)
+
+ const request = objectStore.getAll()
+
+ request.onsuccess = (event: Event) => {
+ callback((event.target as IDBRequest).result)
+ }
+
+ request.onerror = (event: Event) => {
+ console.error('Error retrieving all notifications:', (event.target as IDBRequest).error)
+ }
+ }
+
+ return {
+ addPhoto,
+ addNotification,
+ getPhoto,
+ getNotification,
+ deletePhotos,
+ deleteNotifications,
+ getAllPhotos,
+ getAllNotifications,
+ isAddedPhoto,
+ }
+}
+
+export default useIndexedDB
diff --git a/src/shared/lib/hooks/useLoader.ts b/src/shared/lib/hooks/useLoader.ts
index 0f73db38..a5875d5a 100644
--- a/src/shared/lib/hooks/useLoader.ts
+++ b/src/shared/lib/hooks/useLoader.ts
@@ -1,6 +1,7 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
+// eslint-disable-next-line import/no-named-as-default
import NProgress from 'nprogress'
export const useLoader = () => {
diff --git a/src/shared/lib/hooks/usePagination.ts b/src/shared/lib/hooks/usePagination.ts
new file mode 100644
index 00000000..db6efb38
--- /dev/null
+++ b/src/shared/lib/hooks/usePagination.ts
@@ -0,0 +1,18 @@
+import { useState } from 'react'
+
+const usePagination = () => {
+ const [currentPage, setCurrentPage] = useState(1)
+ const [pageSize, setPageSize] = useState(10)
+ const [sortBy, setSortBy] = useState('createdAt')
+
+ return {
+ currentPage,
+ setCurrentPage,
+ pageSize,
+ setPageSize,
+ sortBy,
+ setSortBy,
+ }
+}
+
+export default usePagination
diff --git a/src/shared/lib/hooks/useSortBy.tsx b/src/shared/lib/hooks/useSortBy.tsx
new file mode 100644
index 00000000..cf8e53e7
--- /dev/null
+++ b/src/shared/lib/hooks/useSortBy.tsx
@@ -0,0 +1,27 @@
+import React, { useState } from 'react'
+
+import { Filter } from '@/shared/assets/icons/Filter'
+import { Polygon } from '@/shared/assets/icons/Polygon'
+import { PolygonUp } from '@/shared/assets/icons/PolygonUp'
+import { SortDirection } from '@/shared/constants/enum'
+
+export const useSortBy = () => {
+ const [sort, setSort] = useState(SortDirection.DESC)
+ const [activeKey, setActiveKey] = useState(null)
+
+ const onSortChange = (key: string) => {
+ setActiveKey(key)
+ if (sort === SortDirection.DESC) setSort(SortDirection.ASC)
+ if (sort === SortDirection.ASC) setSort('default')
+ if (sort === 'default') setSort(SortDirection.DESC)
+ }
+
+ const icon = (key: string) => {
+ if (activeKey !== key) return
+ if (sort === SortDirection.DESC) return
+ if (sort === SortDirection.ASC) return
+ if (sort === 'default') return
+ }
+
+ return { icon, onSortChange, sort }
+}
diff --git a/src/shared/lib/hooks/useVisibleItems.ts b/src/shared/lib/hooks/useVisibleItems.ts
new file mode 100644
index 00000000..e5f5f673
--- /dev/null
+++ b/src/shared/lib/hooks/useVisibleItems.ts
@@ -0,0 +1,38 @@
+import { useEffect, useState } from 'react'
+
+export const useVisibleItems = (items: MessagesNotif[], toggle: boolean) => {
+ const [visibleItems, setVisibleItems] = useState([])
+
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ entries => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const itemId = Number(entry.target.getAttribute('data-id'))
+
+ setVisibleItems(prevVisibleItems => {
+ if (!prevVisibleItems.includes(itemId)) {
+ return [...prevVisibleItems, itemId]
+ }
+
+ return prevVisibleItems
+ })
+ }
+ })
+ },
+ {
+ threshold: 0.1,
+ }
+ )
+
+ const elements = document.querySelectorAll('[data-id]')
+
+ elements.forEach(element => observer.observe(element))
+
+ return () => {
+ elements.forEach(element => observer.unobserve(element))
+ }
+ }, [items])
+
+ return toggle ? visibleItems : []
+}
diff --git a/src/shared/lib/utils/addLangValue.ts b/src/shared/lib/utils/addLangValue.ts
new file mode 100644
index 00000000..b7c8a80e
--- /dev/null
+++ b/src/shared/lib/utils/addLangValue.ts
@@ -0,0 +1,30 @@
+export function addLangValue(lang: LangType, value: ValueType | ValuePriceType): T {
+ const arrayValue = {
+ en: {
+ Personal: 'Personal',
+ Business: 'Business',
+ Персональный: 'Personal',
+ Бизнес: 'Business',
+ '$10 per 1 Day': '$10 per 1 Day',
+ '$50 per 7 Day': '$50 per 7 Day',
+ '$100 per month': '$100 per month',
+ '10$ за один день': '$10 per 1 Day',
+ '50$ за неделю': '$50 per 7 Day',
+ '100$ за месяц': '$100 per month',
+ } as const,
+ ru: {
+ Персональный: 'Персональный',
+ Бизнес: 'Бизнес',
+ Personal: 'Персональный',
+ Business: 'Бизнес',
+ '10$ за один день': '10$ за один день',
+ '50$ за неделю': '50$ за неделю',
+ '100$ за месяц': '100$ за месяц',
+ '$10 per 1 Day': '10$ за один день',
+ '$50 per 7 Day': '50$ за неделю',
+ '$100 per month': '100$ за месяц',
+ } as const,
+ }
+
+ return arrayValue[lang][value] as T
+}
diff --git a/src/shared/locales/en.ts b/src/shared/locales/en.ts
index a2638089..2c370a35 100644
--- a/src/shared/locales/en.ts
+++ b/src/shared/locales/en.ts
@@ -12,6 +12,7 @@ export const en = {
statistics: 'Statistics',
favorites: 'Favorites',
profile_btn: 'Profile Settings',
+ View_all_comments: 'View all comments',
},
resend: {
title: 'Email verification link expired',
@@ -120,8 +121,14 @@ export const en = {
settings: 'Profile Settings',
log_out: 'Log Out',
},
+ sidebarAdmin: {
+ userList: 'User list',
+ paymentsList: 'Payments list',
+ postsList: 'Posts list',
+ statistics: 'Statistics',
+ },
notification_menu: {
- title: 'Notification',
+ title: 'Notifications',
},
add_following: {
// title_of_delete_modal: 'Удалить подписку',
@@ -231,5 +238,112 @@ export const en = {
save_draft: 'Save draft',
add_img_message: 'You have added the maximum number of photos allowed!',
},
+ subscription: {
+ day: '$10 per 1 Day',
+ week: '$50 per 7 Day',
+ month: '$100 per month',
+ },
+ text_subscription_costs: 'Your subscription costs',
+ current_subscription: 'Current Subscription',
+
+ text_account: 'Account type',
+ account_type: {
+ personal: 'Personal',
+ business: 'Business',
+ },
+ text_success: 'Success',
+ payment_success: 'Payment was successful!',
+ button_ok: 'OK',
+
+ text_error: 'Error',
+ transaction_failed: 'Transaction failed. Please, write to support',
+ button_back: 'Back to payment',
+
+ auto_renewal: 'Auto-Renewal',
+ expire_at: 'Expire at',
+ next_payment: 'Next payment',
+ devices: {
+ log_out: 'Log Out',
+ Terminate_sessions: 'Terminate all other sessions',
+ },
+
+ date_of_payment: 'Date of Payment',
+ end_date_of_subscription: 'End date of subscription',
+ amount: 'Amount',
+ price: 'Price',
+ subscription_type: 'Subscription Type',
+ subscription_text: 'Subscription',
+ payment_type: 'Payment Type',
+ payment_method: 'Payment Method',
+ show: 'Show',
+ on_page: 'on page',
+
+ user_list: {
+ id: 'User ID',
+ name: 'Username',
+ profile: 'Profile link',
+ date: 'Date added',
+
+ not_selected: 'Not Selected',
+ blocked: 'Blocked',
+ not_blocked: 'Not Blocked',
+
+ more: 'More information',
+ ban: 'Ban in the system',
+ delete_user: 'Delete user',
+ unban_user: 'Un-ban user',
+ confirmation: 'Are you sure to delete user',
+ confirmation_unBan: 'Are you sure want to un-ban',
+
+ reason_for_ban: 'Reason for ban',
+ bad_behavior: 'Bad behavior',
+ advertising_placement: 'Advertising placement',
+ another_reason: 'Another reason',
+
+ are_you_sure_you: 'Are you sure you want to ban the user',
+ user_blocking: 'User blocking',
+ no: 'No',
+ yes: 'Yes',
+ unBan: 'Un-ban',
+ backToUserList: 'Back to User List',
+ },
+ user_info: {
+ usertId: 'User ID',
+ profileDate: 'Profile Creation Date',
+ uploaded_photos: 'Uploaded photos',
+ payments: 'Payments',
+ followers: 'Followers',
+ following: 'Following',
+ userName: 'UserName',
+ profileLink: 'Profile link',
+ subscriptionDate: 'Subscription Date',
+ not_found: 'Not found',
+ },
+ notification(message: string) {
+ const datePattern = /(\d{2}\/\d{2}\/\d{4})/
+ const match = message?.match(datePattern)
+
+ if (match) {
+ const [month, day, year] = match[0].split('/')
+ const formattedDate = `${day}.${month}.${year}`
+
+ return `Your subscription has been activated and is valid until ${formattedDate}`
+ }
+ const messages = {
+ 'Your subscription-ws ends in 1 day': 'Your subscription-ws ends in 1 day',
+ 'Your subscription ends in 7 days': 'Your subscription ends in 7 days',
+ 'The next subscription payment will be debited from your account after 1 day.':
+ 'The next subscription payment will be debited from your account after 1 day.',
+ }
+
+ return messages[message as keyof typeof messages]
+ },
+ new_notification: 'New notification!',
+ new_title: 'new',
+ today: 'today',
+ hide: 'hide',
+ show_more: 'show more',
+ sendMessage: 'Send Message',
+ publications: 'Publications',
}
export type LangType = typeof en
diff --git a/src/shared/locales/ru.ts b/src/shared/locales/ru.ts
index 670ae3ba..6d6e8540 100644
--- a/src/shared/locales/ru.ts
+++ b/src/shared/locales/ru.ts
@@ -13,6 +13,7 @@ export const ru: LangType = {
statistics: 'Статистика',
favorites: 'Избранное',
profile_btn: 'Настройки профиля',
+ View_all_comments: 'Посмотреть все комментарии',
},
resend: {
title: 'Срок действия ссылки для подтверждения электронной почты истек',
@@ -119,6 +120,12 @@ export const ru: LangType = {
settings: 'Настройки профиля',
log_out: 'Выйти',
},
+ sidebarAdmin: {
+ userList: 'Список пользователей',
+ paymentsList: 'Список платежей',
+ postsList: 'Список постов',
+ statistics: 'Статистика',
+ },
notification_menu: {
title: 'Уведомления',
},
@@ -196,7 +203,7 @@ export const ru: LangType = {
post_view: {
edit: 'Редактировать',
delete: 'Удалить пост',
- answer: 'Ответ',
+ answer: 'Ответить',
like: 'Нравится',
add_comment: 'Добавить комментарий...',
publish: 'Опубликовать',
@@ -230,4 +237,112 @@ export const ru: LangType = {
save_draft: 'Сохранить',
add_img_message: 'Ты добавил максимально допустимое количество фотографий!',
},
+ subscription: {
+ day: '10$ за один день',
+ week: '50$ за неделю',
+ month: '100$ за месяц',
+ },
+ text_subscription_costs: 'Стоимость подписки',
+ current_subscription: 'Текущая подписка',
+
+ text_account: 'Тип аккаунта',
+ account_type: {
+ personal: 'Персональный',
+ business: 'Бизнес',
+ },
+ text_success: 'Успешно',
+ payment_success: 'Оплата прошла успешно!',
+ button_ok: 'ОТЛИЧНО',
+
+ text_error: 'Ошибка',
+ transaction_failed: 'Транзакция не прошла. Пожалуйста, напишите в службу поддержки',
+ button_back: 'Назад к оплате',
+
+ auto_renewal: 'Автопродление',
+ expire_at: 'Истекает',
+ next_payment: 'Следующий платеж',
+ devices: {
+ log_out: 'Выйти',
+ Terminate_sessions: 'Завершить все остальные сеансы',
+ },
+
+ date_of_payment: 'Дата оплаты',
+ end_date_of_subscription: 'Дата окончания подписки',
+ amount: 'Сумма',
+ price: 'Цена',
+ subscription_text: 'Подписка',
+ subscription_type: 'Тип подписки',
+ payment_type: 'Тип оплаты',
+ payment_method: 'Способ оплаты',
+ show: 'Показать',
+ on_page: 'на странице',
+
+ user_list: {
+ id: 'ID пользователя',
+ name: 'Имя пользователя',
+ profile: 'Ссылка профиля',
+ date: 'Дата регистрации',
+
+ not_selected: 'Не выбрано',
+ blocked: 'Заблокировано',
+ not_blocked: 'Не заблокировано',
+ user_blocking: 'Блокировка пользователя',
+ more: 'Подробнее',
+ ban: 'Заблокировать',
+ delete_user: 'Удалить пользователя',
+ confirmation: 'Вы уверены, что хотите удалить пользователя',
+ unBan: 'Разблокировать',
+
+ reason_for_ban: 'Причина блокировки',
+ bad_behavior: 'Плохое поведение',
+ advertising_placement: 'Размещение рекламы',
+ another_reason: 'Другая причина',
+
+ are_you_sure_you: 'Вы уверены, что хотите заблокировать пользователя',
+
+ no: 'Нет',
+ yes: 'Да',
+
+ backToUserList: 'Назад к списку пользователей',
+ unban_user: 'Блокировка пользователя',
+ confirmation_unBan: 'Вы уверены, что хотите снять запрет с ',
+ },
+ user_info: {
+ usertId: 'ID Пользователя',
+ profileDate: 'Дата создания профиля',
+ uploaded_photos: 'Загруженные фотографии',
+ payments: 'Платежи',
+ followers: 'Подписчики',
+ following: 'Подписки',
+ userName: 'Имя пользователя',
+ profileLink: 'Ссылка на профиль',
+ subscriptionDate: 'Дата подписки',
+ not_found: 'Не найдено',
+ },
+ notification(message: string) {
+ const datePattern = /(\d{2}\/\d{2}\/\d{4})/
+ const match = message?.match(datePattern)
+
+ if (match) {
+ const [month, day, year] = match[0].split('/')
+ const formattedDate = `${day}.${month}.${year}`
+
+ return `Ваша подписка активирована и действует до ${formattedDate}`
+ }
+ const messages = {
+ 'Your subscription-ws ends in 1 day': 'Ваша подписка истекает через 1 день',
+ 'Your subscription ends in 7 days': 'Ваша подписка истекает через 7 дней',
+ 'The next subscription payment will be debited from your account after 1 day.':
+ 'Следующий платеж у вас спишется через 1 день',
+ }
+
+ return messages[message as keyof typeof messages]
+ },
+ new_notification: 'Новое уведомление!',
+ new_title: 'Новое',
+ today: 'сегодня',
+ hide: 'скрыть',
+ show_more: 'показать',
+ sendMessage: 'Написать',
+ publications: 'Публикаций',
}
diff --git a/src/shared/types.ts b/src/shared/types.ts
index 4158b0e1..d293ad73 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -50,3 +50,25 @@ export interface IEmailToken {
accessToken: string
email?: string
}
+
+export interface ISubscriptionBody {
+ typeSubscription: 'DAY' | 'WEEKLY' | 'MONTHLY'
+ paymentType: string
+ amount: number
+ baseUrl: string
+}
+
+export interface ISubscriptionBodyWithToken {
+ body: ISubscriptionBody
+ accessToken: string | undefined
+}
+
+export interface IPayments {
+ dateOfPayment: string
+ endDateOfSubscription: string
+ paymentType: string
+ price: number
+ subscriptionId: string
+ subscriptionType: string
+ userId: number
+}
diff --git a/src/widgets/addPostModal/AddPostModal.tsx b/src/widgets/addPostModal/AddPostModal.tsx
index 75b454df..b58a38ed 100644
--- a/src/widgets/addPostModal/AddPostModal.tsx
+++ b/src/widgets/addPostModal/AddPostModal.tsx
@@ -1,4 +1,4 @@
-import React, { ChangeEvent, useRef, useState } from 'react'
+import React, { ChangeEvent, useEffect, useRef, useState } from 'react'
import s from './AddPostModal.module.scss'
@@ -16,6 +16,7 @@ import { Modal } from '@/shared/components/modals'
import { useAppDispatch, useTranslation } from '@/shared/lib'
import { useErrorText } from '@/shared/lib/hooks'
import { useModal } from '@/shared/lib/hooks/open-or-close-hook'
+import useIndexedDB from '@/shared/lib/hooks/useIndexedDB'
import { AddPostModalData } from '@/widgets/addPostModal/addPostModalData'
import { CloseCrop } from '@/widgets/addPostModal/CloseCrop'
import { FilterPublicationModal } from '@/widgets/addPostModal/filterModal/FilterPublicatioModal'
@@ -46,14 +47,27 @@ export const readFile = (file: File) => {
})
}
export const AddPostModal = ({ openPostModal, closePostModal }: Props) => {
+ const { t } = useTranslation()
+
const [imageSrc, setImageSrc] = useState(null)
const [openCloseCrop, setCloseCropModal] = useState(false)
+ const [isDraft, setIsDraft] = useState(false)
+ const [modalPost, setModalPost] = useState(false)
+
const { isOpen, openModal, closeModal } = useModal()
+ const { addPhoto, deletePhotos, getAllPhotos, isAddedPhoto } = useIndexedDB('photoGalleryDB', {
+ photoStore: 'photos',
+ })
const { selectPhotoHandler, inputRef } = useGeneralInputRefForPost()
const croppers = useAppSelector(state => state.croppersSlice)
const { errorText, showErrorText } = useErrorText()
const dispatch = useAppDispatch()
- const { t } = useTranslation()
+
+ getAllPhotos(photos => {
+ if (photos.length) {
+ setIsDraft(true)
+ }
+ })
const handleCloseFilter = () => {
croppers.forEach(cropper => {
@@ -94,14 +108,30 @@ export const AddPostModal = ({ openPostModal, closePostModal }: Props) => {
}
let imageDataUrl: any = await readFile(file)
- setImageSrc(imageDataUrl)
+ addPhoto(imageDataUrl)
+
addNewCropper(imageDataUrl)
+ setImageSrc(imageDataUrl)
+ setModalPost(true)
+ setIsDraft(true)
+
+ if (inputRef.current) {
+ inputRef.current.value = ''
+ }
}
}
const handleBack = () => {
- dispatch(removeAllPhotos())
- setImageSrc(null)
+ if (!croppers[0].originalImage) {
+ setImageSrc(null)
+ setIsDraft(false)
+ setModalPost(false)
+ } else {
+ dispatch(removeAllPhotos())
+ deletePhotos()
+ setModalPost(false)
+ setIsDraft(false)
+ }
}
const handleClosePostCropModal = () => {
closePostModal()
@@ -123,39 +153,79 @@ export const AddPostModal = ({ openPostModal, closePostModal }: Props) => {
}
const handleOpenFilter = async () => {
- await croppers.forEach(cropper => {
+ croppers.forEach(cropper => {
dispatch(setOriginalImage(cropper.image))
})
await addNewCropperForFilter()
openModal()
+ setModalPost(false)
+ setIsDraft(true)
}
const handleInteractOutsideOfCrop = (event: Event) => {
event.preventDefault()
imageSrc && setCloseCropModal(true)
+ isAddedPhoto && modalPost && setCloseCropModal(true)
+ }
+
+ const draftPhotoHandler = () => {
+ setModalPost(true)
+ setIsDraft(true)
+ }
+
+ const onDiscordHandle = () => {
+ deletePhotos()
+ dispatch(removeAllPhotos())
+ setImageSrc(null)
+ setModalPost(false)
+ setIsDraft(false)
+ closePostModal()
+ handleCloseFilter()
+ setCloseCropModal(false)
}
const handleSavePost = () => {
+ setImageSrc(null)
+ setModalPost(false)
closePostModal()
setCloseCropModal(false)
}
+ const onButtonChangePhoto = () => {
+ selectPhotoHandler()
+ setModalPost(true)
+ closeModal()
+ }
+
+ useEffect(() => {
+ if (!croppers.length) {
+ getAllPhotos(photos => {
+ photos.forEach(item => {
+ if (item) {
+ setImageSrc(item.imageUrl)
+ setIsDraft(true)
+ }
+ })
+ })
+ }
+ }, [])
+
return (
<>
setCloseCropModal(false)}
- onDiscard={() => setCloseCropModal(false)}
+ onDiscard={onDiscordHandle}
savePhotoInDraft={handleSavePost}
/>
{
closeFilter={handleCloseFilter}
closeCroppingModal={handleClosePostCropModal}
setImageScr={setImageSrc}
+ setIsDraft={setIsDraft}
+ setModalPost={setModalPost}
/>
- {imageSrc ? (
+ {!isOpen && modalPost ? (
{
-
- {t.post.select_button}
-
-
- {t.post.draft_button}
-
+ {!isDraft ? (
+
+ {t.post.select_button}
+
+ ) : (
+
+ {t.post.draft_button}
+
+ )}
)}
= ({ isOpenFilter }) => {
alt={''}
style={{
filter: post.filterClass,
+ objectFit: 'contain',
}}
+ fill
className={s.postImg}
ref={imageRef}
- width={100}
- height={100}
/>
diff --git a/src/widgets/addPostModal/filterModal/FilterModal.module.scss b/src/widgets/addPostModal/filterModal/FilterModal.module.scss
index 15f01cb1..29987b9c 100644
--- a/src/widgets/addPostModal/filterModal/FilterModal.module.scss
+++ b/src/widgets/addPostModal/filterModal/FilterModal.module.scss
@@ -25,6 +25,7 @@
}
.box {
+ position: relative;
display: flex;
height: 504px;
diff --git a/src/widgets/addPostModal/filterModal/FilterPublicatioModal.tsx b/src/widgets/addPostModal/filterModal/FilterPublicatioModal.tsx
index df76be5f..85288584 100644
--- a/src/widgets/addPostModal/filterModal/FilterPublicatioModal.tsx
+++ b/src/widgets/addPostModal/filterModal/FilterPublicatioModal.tsx
@@ -17,6 +17,7 @@ import {
import { Modal } from '@/shared/components/modals'
import { useAppDispatch, useFetchLoader, useTranslation } from '@/shared/lib'
import { useAuth } from '@/shared/lib/hooks/useAuth'
+import useIndexedDB from '@/shared/lib/hooks/useIndexedDB'
import { CloseCrop } from '@/widgets/addPostModal/CloseCrop'
import { FilteringData } from '@/widgets/addPostModal/filterModal/FilterData'
import { PublicationData } from '@/widgets/addPostModal/publicationModal/PublicationData'
@@ -24,10 +25,11 @@ import { createImage } from '@/widgets/addProfilePhoto/addAvaWithoutRotation/crr
type Props = {
isOpenFilter: boolean
-
closeFilter: () => void
setImageScr: (img: string | null) => void
closeCroppingModal: () => void
+ setIsDraft: (value: boolean) => void
+ setModalPost: (value: boolean) => void
}
export const FilterPublicationModal: FC = ({
@@ -35,6 +37,8 @@ export const FilterPublicationModal: FC = ({
closeCroppingModal,
setImageScr,
closeFilter,
+ setIsDraft,
+ setModalPost,
}) => {
const croppers = useAppSelector(state => state.croppersSlice)
const [openClosCrop, setCloseCrop] = useState(false)
@@ -47,9 +51,16 @@ export const FilterPublicationModal: FC = ({
const [mode, setMode] = useState<'filter' | 'publish'>('filter')
const dispatch = useAppDispatch()
const [isButtonDisabled, setButtonDisabled] = useState(false)
+ const [flag, setFlag] = useState(false)
+ const { deletePhotos } = useIndexedDB('photoGalleryDB', { photoStore: 'photos' })
useFetchLoader(isLoading || isPostLoading)
+ if (!croppers.length || flag) {
+ return
+ }
+
const handleDiscard = () => {
+ setFlag(true)
closeFilter()
setCloseCrop(false)
closeCroppingModal()
@@ -116,7 +127,7 @@ export const FilterPublicationModal: FC = ({
dispatch(setAlert({ variant: 'error', message: error }))
})
}
- const handleInteractOutside = (event: Event) => {
+ const handleInteractOutside = () => {
setCloseCrop(true)
}
const handleSaveFilterPost = () => {
@@ -125,13 +136,27 @@ export const FilterPublicationModal: FC = ({
const handleCloseCrop = () => {
setCloseCrop(false)
}
- const handleOpenNexts = () => {
+ const handleDiscordCrop = () => {
+ deletePhotos()
+ dispatch(removeAllPhotos())
+ setImageScr(null)
+ setIsDraft(false)
+ closeCroppingModal()
+ setCloseCrop(false)
+ }
+ const handleOpenNext = () => {
if (mode === 'filter') {
setMode('publish')
} else {
+ deletePhotos()
+ setIsDraft(false)
handlePublish()
}
}
+ const onCloseFilter = () => {
+ setModalPost(true)
+ closeFilter()
+ }
const onPrevStep = () => {
setMode('filter')
}
@@ -141,15 +166,15 @@ export const FilterPublicationModal: FC = ({
{
onClick={() => {
dispatch(updateFilterClass({ id: idOfImage, filterClass: filter.style }))
}}
+ style={{ position: 'relative' }}
>
-
diff --git a/src/widgets/addPostModal/modalPostHeader/PostHeaderModal.module.scss b/src/widgets/addPostModal/modalPostHeader/PostHeaderModal.module.scss
index 4095f064..73b1c957 100644
--- a/src/widgets/addPostModal/modalPostHeader/PostHeaderModal.module.scss
+++ b/src/widgets/addPostModal/modalPostHeader/PostHeaderModal.module.scss
@@ -25,6 +25,7 @@
@media screen and (width <= 768px) {
font-size: var(--font-size-s);
}
+
@media screen and (width <= 378px) {
font-size: var(--font-size-xs);
}
@@ -36,6 +37,7 @@
height: 16px;
}
}
+
.titleModal {
@media screen and (width<=378px) {
font-size: var(--font-size-s);
diff --git a/src/widgets/addPostModal/modificationTools/tools/post-modification-tools/add-new-photo-tool/AddNewPhotoTool.module.scss b/src/widgets/addPostModal/modificationTools/tools/post-modification-tools/add-new-photo-tool/AddNewPhotoTool.module.scss
index 0b24f17d..3828f6d3 100644
--- a/src/widgets/addPostModal/modificationTools/tools/post-modification-tools/add-new-photo-tool/AddNewPhotoTool.module.scss
+++ b/src/widgets/addPostModal/modificationTools/tools/post-modification-tools/add-new-photo-tool/AddNewPhotoTool.module.scss
@@ -18,16 +18,16 @@
.box {
opacity: 0.8;
}
+
.boxTool {
position: absolute;
- width: 100%;
- top: 87%;
z-index: 12;
+ top: 87%;
left: 90%;
- @media screen and (width<=910px) {
- //top:
- }
+
+ width: 100%;
}
+
.menuBox {
position: relative;
}
diff --git a/src/widgets/addPostModal/modificationTools/tools/post-modification-tools/add-new-photo-tool/AddNewPhotoTool.tsx b/src/widgets/addPostModal/modificationTools/tools/post-modification-tools/add-new-photo-tool/AddNewPhotoTool.tsx
index 9e618b26..ff61657a 100644
--- a/src/widgets/addPostModal/modificationTools/tools/post-modification-tools/add-new-photo-tool/AddNewPhotoTool.tsx
+++ b/src/widgets/addPostModal/modificationTools/tools/post-modification-tools/add-new-photo-tool/AddNewPhotoTool.tsx
@@ -1,6 +1,7 @@
import React, { FC } from 'react'
import { clsx } from 'clsx'
+import Image from 'next/image'
import { Scrollbar } from 'swiper/modules'
import { Swiper, SwiperSlide } from 'swiper/react'
@@ -70,7 +71,7 @@ export const AddNewPhotoTool: FC = ({ selectNewPhoto, closePostModal, set
return (
-
+
handleDeletePhoto(photo.id)}>
{
const { userId, accessToken } = useAuth()
const dispatch = useAppDispatch()
+ const [editedPhotos, setEditedPhotos] = useState([])
const { data: profileData } = useGetProfileQuery({ profileId: +userId, accessToken })
const [wordCount, setWordCount] = useState(0)
const { t } = useTranslation()
@@ -29,6 +31,10 @@ export const PublicationData = ({ photos }: Props) => {
setWordCount(value.length)
}
+ useEffect(() => {
+ setEditedPhotos(photos)
+ }, [photos])
+
return (
@@ -41,11 +47,17 @@ export const PublicationData = ({ photos }: Props) => {
slidesPerView={1}
>
- {photos.map(photo => {
+ {editedPhotos.map(photo => {
return (
-
+
)
@@ -58,7 +70,7 @@ export const PublicationData = ({ photos }: Props) => {
{profileData?.avatars[0] ? (
-
+
) : (
)}
diff --git a/src/widgets/addPostModal/publicationModal/PublicationModal.module.scss b/src/widgets/addPostModal/publicationModal/PublicationModal.module.scss
index fc72b4f9..6d8efaaf 100644
--- a/src/widgets/addPostModal/publicationModal/PublicationModal.module.scss
+++ b/src/widgets/addPostModal/publicationModal/PublicationModal.module.scss
@@ -19,9 +19,12 @@
}
.imageBox {
+ position: relative;
+
display: flex;
align-items: center;
justify-content: center;
+
height: 500px;
@media (width<=768px) {
@@ -47,8 +50,9 @@
.avatar {
width: 36px;
height: 36px;
- border-radius: 50%;
border: 1px solid var(--color-dark-100);
+ border-radius: 50%;
+
@media (width<=768px) {
width: 24px;
height: 24px;
diff --git a/src/widgets/dropDownNotification/ui/DropDownNotification.stories.tsx b/src/widgets/dropDownNotification/ui/DropDownNotification.stories.tsx
index 1a2af984..c54ffb02 100644
--- a/src/widgets/dropDownNotification/ui/DropDownNotification.stories.tsx
+++ b/src/widgets/dropDownNotification/ui/DropDownNotification.stories.tsx
@@ -1,5 +1,3 @@
-import { useState } from 'react'
-
import { Meta } from '@storybook/react'
import { DropDownNotification } from '..'
@@ -13,7 +11,13 @@ export default {
export const DropDownNotificationDefault = () => {
return (
-
+ {}}
+ getAllNotifications={() => {}}
+ accessToken={''}
+ toggle={true}
+ setCount={() => {}}
+ />
)
}
diff --git a/src/widgets/dropDownNotification/ui/DropDownNotification.tsx b/src/widgets/dropDownNotification/ui/DropDownNotification.tsx
index c9f09d84..ef60e8c3 100644
--- a/src/widgets/dropDownNotification/ui/DropDownNotification.tsx
+++ b/src/widgets/dropDownNotification/ui/DropDownNotification.tsx
@@ -1,28 +1,109 @@
-import React from 'react'
+import React, { useEffect, useState } from 'react'
import { clsx } from 'clsx'
+import { isAfter, subMonths } from 'date-fns'
import s from './DropDownNotification.module.scss'
+import {
+ useGetNotificationsQuery,
+ useUpdateNotificationsMutation,
+} from '@/entities/notifications/api/notificationsApi'
+import { SocketApi } from '@/entities/socket/socket-api'
import { Typography } from '@/shared/components'
import { NotificationItem } from '@/shared/components/notification-item/NotificationItem'
import { Scroller } from '@/shared/components/scroller/Scroller'
import { useTranslation } from '@/shared/lib'
+import { useVisibleItems } from '@/shared/lib/hooks/useVisibleItems'
-export const DropDownNotification = ({ toggle }: { toggle: boolean }) => {
+type Props = {
+ toggle: boolean
+ accessToken: string
+ getAllNotifications: (val: (v: MessagesNotif[]) => void) => void
+ deleteNotifications: (keys: number[]) => void
+ setCount: (val: number) => void
+}
+export const DropDownNotification = ({
+ toggle,
+ accessToken,
+ getAllNotifications,
+ deleteNotifications,
+ setCount,
+}: Props) => {
const classNames = clsx(s.dropDownNotification, toggle ? s.active : s.inactive)
const { t } = useTranslation()
+ const [eventNotif, setEventNotif] = useState
([])
+ const visibleItems = useVisibleItems(eventNotif, toggle)
+
+ const { data: notifications, refetch } = useGetNotificationsQuery(accessToken)
+ const [readNotifications] = useUpdateNotificationsMutation()
+
+ const filterNotifications = (notifications: NotificationItems[]) => {
+ const now = new Date()
+ const lastMonth = subMonths(now, 1)
+
+ return notifications?.filter(notification => {
+ const notifyDate = new Date(notification.notifyAt)
+
+ return isAfter(notifyDate, lastMonth)
+ })
+ }
+ const filteredNotifications = filterNotifications(notifications?.items)
+
+ useEffect(() => {
+ getAllNotifications(notif => {
+ if (notif.length === 0) return setEventNotif([])
+ setEventNotif(prev => {
+ const existingIds = prev.map(item => item.id)
+ const uniqueNewItems = notif.filter(item => !existingIds.includes(item.id))
+
+ return prev.concat(uniqueNewItems)
+ })
+ })
+ }, [SocketApi.socket])
+
+ useEffect(() => {
+ if (visibleItems.length > 0) {
+ readNotifications({ body: { ids: visibleItems }, accessToken })
+ setCount(0)
+ }
+
+ return () => {
+ deleteNotifications(visibleItems)
+ if (visibleItems.length > 0) {
+ refetch()
+ }
+ }
+ }, [visibleItems])
+
return (
{t.notification_menu.title}
- {Array.from({ length: 10 }, (_, i) => (
-
+ {eventNotif.map(item => (
+
))}
+ {filteredNotifications
+ ?.filter((item: NotificationItems) => item.isRead)
+ .map((notification: NotificationItems) => {
+ return (
+
+ )
+ })}
)
diff --git a/src/widgets/header/ui/HeaderWidget.tsx b/src/widgets/header/ui/HeaderWidget.tsx
index a581feb4..de3e6f45 100644
--- a/src/widgets/header/ui/HeaderWidget.tsx
+++ b/src/widgets/header/ui/HeaderWidget.tsx
@@ -7,12 +7,14 @@ import { useRouter } from 'next/router'
import s from './HeaderWidget.module.scss'
import { useLogOutMutation } from '@/entities/auth'
+import { SocketApi } from '@/entities/socket/socket-api'
import { BookMarkIcon, FavoritesIcon, LogOutIcon, StatisticsIcon } from '@/shared/assets'
import { ProfileSettings } from '@/shared/assets/icons/ProfileSettings'
import { Button, CustomDropdown, CustomDropdownItem, Typography } from '@/shared/components'
import { NotificationBell } from '@/shared/components/notificatification-bell'
import { useTranslation } from '@/shared/lib'
import { useAuth } from '@/shared/lib/hooks/useAuth'
+import useIndexedDB from '@/shared/lib/hooks/useIndexedDB'
import { DropDownNotification } from '@/widgets/dropDownNotification'
import { LangSelectWidget } from '@/widgets/langSelect'
@@ -21,11 +23,22 @@ export const HeaderWidget: FC = () => {
const menuRef = useRef(null)
const { t } = useTranslation()
+
const [logOut] = useLogOutMutation()
+ const [count, setCount] = useState(0)
const { isAuth, accessToken } = useAuth()
+ const { addNotification, getAllNotifications, deleteNotifications } = useIndexedDB('myDatabase', {
+ notificationStore: 'notification',
+ })
const router = useRouter()
+ const updateStateNotifications = () => {
+ getAllNotifications(notif => {
+ setCount(notif.length)
+ })
+ }
+
useEffect(() => {
const handler = (e: MouseEvent): void => {
!menuRef.current?.contains(e.target as Node) && setToggle(false)
@@ -33,16 +46,26 @@ export const HeaderWidget: FC = () => {
document.addEventListener('mousedown', handler)
+ if (isAuth) {
+ SocketApi.creatConnection(accessToken as string)
+
+ SocketApi.socket?.on('notifications', event => {
+ if (event.length !== 0) {
+ addNotification(event, updateStateNotifications)
+ }
+ })
+ }
+
return () => {
document.removeEventListener('mousedown', handler)
}
- }, [])
+ }, [accessToken, SocketApi.socket, addNotification])
return (
-
+
{isAuth ? (
Inctagram
@@ -56,8 +79,14 @@ export const HeaderWidget: FC = () => {
{isAuth && (
-
-
+
+
)}
diff --git a/src/widgets/headerAdmin/index.ts b/src/widgets/headerAdmin/index.ts
new file mode 100644
index 00000000..597dba11
--- /dev/null
+++ b/src/widgets/headerAdmin/index.ts
@@ -0,0 +1 @@
+export { HeaderAdmin } from './ui/HeaderAdmin'
diff --git a/src/widgets/headerAdmin/ui/HeaderAdmin.module.scss b/src/widgets/headerAdmin/ui/HeaderAdmin.module.scss
new file mode 100644
index 00000000..0f323572
--- /dev/null
+++ b/src/widgets/headerAdmin/ui/HeaderAdmin.module.scss
@@ -0,0 +1,53 @@
+.content {
+ cursor: pointer;
+
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ justify-content: flex-start;
+
+ &:active {
+ color: var(--color-accent-500);
+ fill: var(--color-accent-500);
+ stroke: var(--color-accent-500);
+ }
+
+ &:hover {
+ color: var(--color-accent-100);
+ stroke: var(--color-accent-100);
+ }
+
+ &:focus-visible {
+ border-radius: 2px;
+ outline: 2px solid var(--color-accent-700);
+ }
+
+ &:disabled {
+ color: var(--color-dark-100);
+ stroke: var(--color-dark-100);
+ }
+}
+
+.marginBox {
+ margin-bottom: 60px;
+}
+
+.activeLink {
+ font-size: 14px;
+ font-weight: 700;
+ font-style: normal;
+ line-height: 24px;
+ color: var(--color-accent-500);
+}
+
+.wrappedActionMenu {
+ @media (width >1023px) {
+ display: none;
+ }
+}
+
+.buttonWrapper {
+ @media (width <= 1023px) {
+ display: none;
+ }
+}
diff --git a/src/widgets/headerAdmin/ui/HeaderAdmin.tsx b/src/widgets/headerAdmin/ui/HeaderAdmin.tsx
new file mode 100644
index 00000000..a05c26c8
--- /dev/null
+++ b/src/widgets/headerAdmin/ui/HeaderAdmin.tsx
@@ -0,0 +1,44 @@
+import React, { FC, useEffect, useRef, useState } from 'react'
+
+import Link from 'next/link'
+
+import { useAdmin } from '@/shared/lib/hooks/useAdmin'
+import { LangSelectWidget } from '@/widgets/langSelect'
+
+export const HeaderAdmin: FC = () => {
+ const [toggle, setToggle] = useState(false)
+
+ const menuRef = useRef
(null)
+
+ const isAdmin = useAdmin()
+
+ useEffect(() => {
+ const handler = (e: MouseEvent): void => {
+ !menuRef.current?.contains(e.target as Node) && setToggle(false)
+ }
+
+ document.addEventListener('mousedown', handler)
+
+ return () => {
+ document.removeEventListener('mousedown', handler)
+ }
+ }, [])
+
+ return (
+
+ )
+}
diff --git a/src/widgets/imageList/ui/ImageListUI.tsx b/src/widgets/imageList/ui/ImageListUI.tsx
index 43fccd32..be944252 100644
--- a/src/widgets/imageList/ui/ImageListUI.tsx
+++ b/src/widgets/imageList/ui/ImageListUI.tsx
@@ -1,7 +1,7 @@
import { ImageCard } from '@/shared/components/imageCard'
type Props = {
- posts: PostDataToComponent[]
+ posts: any[]
openModal?: (id: number) => void
}
diff --git a/src/widgets/layouts/header-with-sidebar-layout/HeaderWithSidebarLayout.tsx b/src/widgets/layouts/header-with-sidebar-layout/HeaderWithSidebarLayout.tsx
index e5f662fa..79429999 100644
--- a/src/widgets/layouts/header-with-sidebar-layout/HeaderWithSidebarLayout.tsx
+++ b/src/widgets/layouts/header-with-sidebar-layout/HeaderWithSidebarLayout.tsx
@@ -9,8 +9,10 @@ import s from './HeaderWithSidebarLayout.module.scss'
import { Scroller } from '@/shared/components/scroller/Scroller'
import { Sidebar } from '@/shared/components/sidebar'
+import { useAdmin } from '@/shared/lib/hooks/useAdmin'
import { useAuth } from '@/shared/lib/hooks/useAuth'
import { useClient } from '@/shared/lib/hooks/useClient'
+
type Props = {
children: ReactNode
}
@@ -19,11 +21,11 @@ export const HeaderWithSidebarLayout: FC = ({ children }) => {
const router = useRouter()
const { isAuth } = useAuth()
const { isClient } = useClient()
+ // const { isAdmin } = useAdmin()
useEffect(() => {
if (!isAuth && isClient) router.push('/signin')
}, [isAuth, isClient, router])
-
if (!isAuth) return null
return (
diff --git a/src/widgets/layouts/superAdmin-layout/InfoUserLayout/InfoUserLayout.tsx b/src/widgets/layouts/superAdmin-layout/InfoUserLayout/InfoUserLayout.tsx
new file mode 100644
index 00000000..dfa8ff28
--- /dev/null
+++ b/src/widgets/layouts/superAdmin-layout/InfoUserLayout/InfoUserLayout.tsx
@@ -0,0 +1,38 @@
+import { FC, ReactElement, ReactNode, useEffect } from 'react'
+
+import { useRouter } from 'next/router'
+
+import { Scroller } from '@/shared/components/scroller/Scroller'
+import { HeaderAdmin } from '@/widgets/headerAdmin'
+import s from '@/widgets/layouts/superAdmin-layout/SuperAdminLayout.module.scss'
+
+type Props = {
+ children: ReactNode
+}
+
+export const InfoUserLayout: FC = ({ children }) => {
+ const router = useRouter()
+
+ useEffect(() => {
+ if (!localStorage.getItem('isAdmin')) {
+ router.push('/signin')
+ }
+ }, [router])
+
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+export const getInfoUserLayout = (page: ReactElement) => {
+ return {page}
+}
diff --git a/src/widgets/layouts/superAdmin-layout/SuperAdminLayout.module.scss b/src/widgets/layouts/superAdmin-layout/SuperAdminLayout.module.scss
new file mode 100644
index 00000000..8309ec63
--- /dev/null
+++ b/src/widgets/layouts/superAdmin-layout/SuperAdminLayout.module.scss
@@ -0,0 +1,27 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ background-color: var(--color-dark-700);
+
+ .main {
+ display: flex;
+ flex-grow: 1;
+ max-height: calc(100vh - 4rem);
+ }
+
+ .sidebar {
+ @media (width < 1024px) {
+ display: none;
+ }
+ }
+
+ .header {
+ position: relative;
+ }
+
+ .wrapperContent {
+ overflow: auto;
+ width: 100%;
+ }
+}
diff --git a/src/widgets/layouts/superAdmin-layout/SuperAdminLayout.tsx b/src/widgets/layouts/superAdmin-layout/SuperAdminLayout.tsx
new file mode 100644
index 00000000..8ad4f263
--- /dev/null
+++ b/src/widgets/layouts/superAdmin-layout/SuperAdminLayout.tsx
@@ -0,0 +1,43 @@
+import { FC, ReactElement, ReactNode, useEffect } from 'react'
+
+import { useRouter } from 'next/router'
+
+import s from './SuperAdminLayout.module.scss'
+
+import { Scroller } from '@/shared/components/scroller/Scroller'
+import { SidebarAdmin } from '@/shared/components/sidebarAdmin/SidebarAdmin'
+import { useAppSelector } from '@/shared/lib'
+import { HeaderAdmin } from '@/widgets/headerAdmin/ui/HeaderAdmin'
+
+type Props = {
+ children: ReactNode
+}
+export const SuperAdminLayout: FC = ({ children }) => {
+ const router = useRouter()
+
+ useEffect(() => {
+ if (!localStorage.getItem('isAdmin')) {
+ router.push('/signin')
+ }
+ }, [router])
+
+ return (
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+export const getSuperAdminLayoutLayout = (page: ReactElement) => {
+ return {page}
+}
diff --git a/src/widgets/logOut/ui/LogOutWidget.tsx b/src/widgets/logOut/ui/LogOutWidget.tsx
index cee78282..3e75d0bf 100644
--- a/src/widgets/logOut/ui/LogOutWidget.tsx
+++ b/src/widgets/logOut/ui/LogOutWidget.tsx
@@ -15,10 +15,12 @@ export const LogOutWidget: FC<{ onClose: () => void }> = ({ onClose }) => {
const { accessToken } = useAuth()
const router = useRouter()
+ const isAdmin = useAppSelector(store => store.adminSlice.isAdmin)
+
return (
{
@@ -36,7 +38,7 @@ export const LogOutWidget: FC<{ onClose: () => void }> = ({ onClose }) => {
- {t.logout.message} {email}?
+ {isAdmin ? 'Вы хотите выйти из админки ? ' : `${t.logout.message} ${email}?`}
{
+ const { data: dataAnswer } = useGetAnswerQuery({ postId: id, commentId: el.id, accessToken })
+ const [createAnswerLike, { isLoading: createAnswerLikeLoading }] = useLikeAnswerMutation()
+
+ console.log(dataAnswer && dataAnswer.items)
+ const [isHideAnswer, setIsHideAnswer] = useState(false)
+ const [like, setLike] = useState<'LIKE' | 'NONE'>('NONE')
+ const clickHeandler = () => {
+ setIsHideAnswer(!isHideAnswer)
+ }
+ const submitClickHandler = (answerId: number) => {
+ if (like === 'NONE') {
+ setLike('LIKE')
+ createAnswerLike({
+ commentId: el.id,
+ likeStatus: 'LIKE',
+ postId: el.postId,
+ answerId: answerId,
+ accessToken,
+ })
+ } else {
+ setLike('NONE')
+ createAnswerLike({
+ commentId: el.id,
+ likeStatus: 'NONE',
+ postId: el.postId,
+ answerId: answerId,
+ accessToken,
+ })
+ }
+ }
+
+ if (!dataAnswer) return null
+
+ return (
+ <>
+ {dataAnswer.totalCount !== 0 ? (
+
+ - Show answers ({dataAnswer.totalCount})
+
+ ) : (
+ ''
+ )}
+
+ {dataAnswer.items.map((el: any) => (
+
+
+
+
+
+
+ {el.from.username}
+
+
+
+
+ {el.content}
+
+
+
+
+
+
+
+ Like: {el.likeCount}
+
+
+ {/**/}
+ {/* {t.post_view.answer}*/}
+ {/* */}
+
+
+
+
+
submitClickHandler(el.id)}>
+ {el.isLiked ? : }
+
+
+
+ ))}
+ >
+ )
+}
diff --git a/src/widgets/postViewModal/UI/ModalContentUI.tsx b/src/widgets/postViewModal/UI/ModalContentUI.tsx
index 2ea5b11c..dc64d557 100644
--- a/src/widgets/postViewModal/UI/ModalContentUI.tsx
+++ b/src/widgets/postViewModal/UI/ModalContentUI.tsx
@@ -26,10 +26,13 @@ export const ModalContentUI = ({ data }: Props) => {
/>
)}
-
{data && }
+
+ {data && }
+
{data && (
({})}
ownerId={data.ownerId}
diff --git a/src/widgets/postViewModal/UI/ModalContentWithEditUI.tsx b/src/widgets/postViewModal/UI/ModalContentWithEditUI.tsx
index 80471765..3aa50163 100644
--- a/src/widgets/postViewModal/UI/ModalContentWithEditUI.tsx
+++ b/src/widgets/postViewModal/UI/ModalContentWithEditUI.tsx
@@ -101,11 +101,14 @@ export const ModalContentWithEditUI = ({
)}
- {data && data.id === modalId && }
+ {data && data.id === modalId && (
+
+ )}
{data && (
void
+ setCommentId?: (answerId: number) => void
+ id?: number
+ oneComments: boolean
+ home?: boolean
+}
+export const PostAuthorizedAndUnauthorized = ({
+ t,
+ el,
+ setIsAnswer,
+ setCommentId,
+ oneComments,
+ home,
+}: Props) => {
+ const { accessToken } = useAuth()
+ const [createLike, { isLoading: isPostLoading }] = useLikeCommentMutation()
+ const [like, setLike] = useState<'LIKE' | 'NONE'>('NONE')
+ const submitClickHandler = () => {
+ if (like === 'NONE') {
+ setLike('LIKE')
+ createLike({
+ commentId: el.id,
+ likeStatus: 'LIKE',
+ postId: el.postId,
+ accessToken,
+ })
+ } else {
+ setLike('NONE')
+ createLike({
+ commentId: el.id,
+ likeStatus: 'NONE',
+ postId: el.postId,
+ accessToken,
+ })
+ }
+ // useUpdateLikeStatus({el,accessToken,createLike})
+ }
+
+ const clickHandlerAnswer = () => {
+ setCommentId && setCommentId(el.id)
+ setIsAnswer && setIsAnswer(true)
+ }
+
+ return (
+
+
+
+
+
+
+
+ {el.from.username}
+
+
+
+
+ {el.content}
+
+ {oneComments ? (
+ ''
+ ) : (
+ <>
+
+
+
+
+
+
+ Like: {el.likeCount}
+
+
+
+ {t.post_view.answer}
+
+
+ >
+ )}
+
+
+ {oneComments ? (
+ ''
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/src/widgets/postViewModal/UI/PostCommentsView.module.scss b/src/widgets/postViewModal/UI/PostCommentsView.module.scss
index 535a7430..c88b386e 100644
--- a/src/widgets/postViewModal/UI/PostCommentsView.module.scss
+++ b/src/widgets/postViewModal/UI/PostCommentsView.module.scss
@@ -3,9 +3,8 @@
align-items: center;
justify-content: space-between;
+ width: 30.5rem;
padding: 12px 24px;
-
- border-bottom: 1px solid var(--color-dark-100);
}
.headerOnMiddle {
@@ -33,6 +32,11 @@
background-color: var(--color-dark-300);
}
+.footer {
+ width: 30.5rem;
+ background-color: var(--color-dark-300);
+}
+
@media (width <= 576px) {
.main {
height: 190px;
@@ -55,9 +59,34 @@
.smallAvatarPost {
transform: translateY(5px);
+
+ min-width: 36px;
+ min-height: 36px;
+ margin-left: 1.5rem;
+ padding-top: 0.3rem;
+
border-radius: 50%;
}
+.smallAvatarPostHome {
+ transform: translateY(5px);
+
+ min-width: 36px;
+ min-height: 36px;
+ padding-top: 0.3rem;
+
+ border-radius: 50%;
+}
+
+.postContent {
+ margin-right: 5rem;
+}
+
+.postContentHome {
+ width: 30rem;
+ margin-right: 1rem;
+}
+
.dots {
cursor: pointer;
color: var(--color-accent-500);
@@ -72,6 +101,12 @@
justify-content: flex-start;
}
+.imageContainer {
+ width: 710px;
+ height: 491px;
+ padding-right: 13.9rem;
+}
+
.button {
margin: 0;
padding: 0;
@@ -89,9 +124,51 @@
}
.updatedAt {
+ display: initial;
+ padding-left: 5px;
+ color: var(--color-light-900);
+}
+
+.allComment {
+ padding-left: 1.5rem;
color: var(--color-light-900);
}
+.InputField {
+ background-color: transparent;
+ border: none;
+ outline: none;
+}
+
+.answer {
+ overflow-x: hidden;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+
+ max-width: 480px;
+ margin-left: 3rem;
+}
+
+.answerNone {
+ display: none;
+}
+
+.buttonAnswer {
+ display: flex;
+ justify-content: space-evenly;
+
+ width: 14rem;
+ padding-bottom: 1px;
+
+ font-size: 0.8rem;
+ color: gray;
+
+ &:hover {
+ color: gainsboro;
+ }
+}
+
.comment {
overflow-x: hidden;
display: flex;
@@ -102,13 +179,13 @@
}
.like {
+ cursor: pointer;
padding: 24px 24px 0 0;
}
.share {
+ width: 30.5rem;
padding: 12px 24px;
- border-top: 1px solid var(--color-dark-100);
- border-bottom: 1px solid var(--color-dark-100);
}
.shareIcons {
@@ -163,5 +240,7 @@
display: flex;
align-items: center;
justify-content: space-between;
+
+ width: 30.5rem;
padding: 12px 24px;
}
diff --git a/src/widgets/postViewModal/UI/PostCommentsView.tsx b/src/widgets/postViewModal/UI/PostCommentsView.tsx
index b8437fbf..87eb3e86 100644
--- a/src/widgets/postViewModal/UI/PostCommentsView.tsx
+++ b/src/widgets/postViewModal/UI/PostCommentsView.tsx
@@ -1,14 +1,23 @@
+import React, { useEffect, useState } from 'react'
+
import Image from 'next/image'
import Link from 'next/link'
import s from './PostCommentsView.module.scss'
+import {
+ useCreateAnswerMutation,
+ useGetAnswerQuery,
+ useGetCommentQuery,
+ useGetCommentUnAuthorizationQuery,
+ useUpdateCommentMutation,
+} from '@/entities/comments/api/commentsApi'
+import { InputField } from '@/shared'
import {
BookmarkOutlineIcon,
DeletePostIcon,
EditPostIcon,
HeartOutline,
- HeartRed,
TelegramIcon,
} from '@/shared/assets'
import ThreeDots from '@/shared/assets/icons/three-dots.png'
@@ -18,6 +27,8 @@ import {
Button,
CustomDropdown,
CustomDropdownItem,
+ SwiperSlider,
+ Textarea,
TimeAgo,
Typography,
} from '@/shared/components'
@@ -25,8 +36,11 @@ import { AvatarSmallView } from '@/shared/components/avatarSmallView'
import { Scroller } from '@/shared/components/scroller/Scroller'
import { useFormatDate, useTranslation } from '@/shared/lib'
import { useAuth } from '@/shared/lib/hooks/useAuth'
+import { AnswerData } from '@/widgets/postViewModal/UI/AnswerData'
+import { PostAuthorizedAndUnauthorized } from '@/widgets/postViewModal/UI/PostAuthorizedAndUnauthorized'
type Props = {
+ id?: number
ownerId: number
avatarOwner: string
userName: string
@@ -34,8 +48,8 @@ type Props = {
updatedAt: string
isSSR: boolean
- setModalType: (modalType: 'edit' | 'view') => void
- openDeleteModal: () => void
+ setModalType?: (modalType: 'edit' | 'view') => void
+ openDeleteModal?: () => void
}
type PostModalHeaderProps = Omit
@@ -54,10 +68,10 @@ export const PostModalHeader = ({
return (
-
-
-
{userName}
-
+ {/*
*/}
+ {/*
*/}
+ {/*
{userName} */}
+ {/**/}
{isAuth && userId == ownerId && !isSSR && (
@@ -66,12 +80,20 @@ export const PostModalHeader = ({
align={'end'}
>
- setModalType('edit')}>
+ (setModalType ? setModalType('edit') : '')}
+ >
{t.post_view.edit}
- openDeleteModal()}>
+ (openDeleteModal ? openDeleteModal() : '')}
+ >
{t.post_view.delete}
@@ -91,9 +113,40 @@ export const PostCommentsView = ({
isSSR,
setModalType,
openDeleteModal,
+ id,
}: Props) => {
const { t } = useTranslation()
const { formatDate } = useFormatDate(t.lg)
+ const [updateComments, { isLoading: isPostLoading }] = useUpdateCommentMutation()
+ const [createAnswer, { isLoading: isCreateAnswer }] = useCreateAnswerMutation()
+ const { accessToken } = useAuth()
+ const [comment, setComment] = useState
('')
+ const { data: dataAuth } = useGetCommentQuery({ postId: id, accessToken })
+ const { data, isLoading, error } = useGetCommentUnAuthorizationQuery({ postId: id })
+ const { isAuth } = useAuth()
+ const [isAnswer, setIsAnswer] = useState(false)
+ const [commentId, setCommentId] = useState()
+ const submitClickHandler = () => {
+ setComment('')
+ if (isAnswer) {
+ createAnswer({
+ content: comment,
+ commentId: commentId,
+ postId: id,
+ accessToken,
+ })
+ setIsAnswer(false)
+ } else
+ updateComments({
+ content: comment,
+ postId: id,
+ accessToken,
+ })
+ }
+
+ useEffect(() => {
+ if (isAnswer) return setComment('@' + userName + ',')
+ }, [isAnswer])
return (
@@ -107,10 +160,12 @@ export const PostCommentsView = ({
openDeleteModal={openDeleteModal}
/>
+
+
@@ -126,117 +181,42 @@ export const PostCommentsView = ({
-
-
-
-
-
-
- URLProfiele
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
- incididunt ut labore et dolore magna aliqua
-
-
-
-
-
-
-
- {t.post_view.answer}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- URLProfiele
-
- {' '}
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
- incididunt ut labore et dolore magna aliqua
-
-
-
-
- {' '}
-
-
- {t.post_view.like}: 1
-
-
-
- {t.post_view.answer}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- URLProfiele
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
- incididunt ut labore et dolore magna aliqua
-
-
-
-
-
-
-
- {t.post_view.answer}
-
-
-
-
-
-
-
-
+ {isAuth
+ ? dataAuth &&
+ dataAuth.items.map((el: CommentsDataType) => (
+ <>
+
+
+ >
+ ))
+ : data &&
+ data.items.map((el: any) => (
+
+ ))}
-
+
diff --git a/src/widgets/postViewModal/updateLikeStatus/useUpdateLikeStatus.ts b/src/widgets/postViewModal/updateLikeStatus/useUpdateLikeStatus.ts
new file mode 100644
index 00000000..3968aae9
--- /dev/null
+++ b/src/widgets/postViewModal/updateLikeStatus/useUpdateLikeStatus.ts
@@ -0,0 +1,34 @@
+import { useEffect, useState } from 'react'
+
+type Props = {
+ el: any
+ accessToken: any
+ createLike?: any
+ createAnswerLike?: any
+}
+
+export const useUpdateLikeStatus = ({ el, accessToken, createLike }: Props) => {
+ const [like, setLike] = useState<'LIKE' | 'NONE'>('NONE')
+
+ useEffect(() => {
+ if (like === 'NONE') {
+ setLike('LIKE')
+ createLike({
+ commentId: el.id,
+ likeStatus: 'LIKE',
+ postId: el.postId,
+ accessToken,
+ })
+ } else {
+ setLike('NONE')
+ createLike({
+ commentId: el.id,
+ likeStatus: 'NONE',
+ postId: el.postId,
+ accessToken,
+ })
+ }
+ }, [createLike])
+
+ return null
+}
diff --git a/src/widgets/profileHeader/ui/ProfileHeaderWeb.tsx b/src/widgets/profileHeader/ui/ProfileHeaderWeb.tsx
index 3487e7d2..f0e49a23 100644
--- a/src/widgets/profileHeader/ui/ProfileHeaderWeb.tsx
+++ b/src/widgets/profileHeader/ui/ProfileHeaderWeb.tsx
@@ -3,11 +3,13 @@ import Link from 'next/link'
import s from './ProfileHeaderWeb.module.scss'
+import { useGetUserNameQuery } from '@/entities/users-follow/api/usersFollowApi'
import { DefaultProfileImg } from '@/shared/assets'
import { Typography, Button } from '@/shared/components'
import { ModalOfFollowers } from '@/shared/components/followers-modal'
import { ModalOfFollowing } from '@/shared/components/following-modal'
import { useTranslation } from '@/shared/lib'
+import { useAuth } from '@/shared/lib/hooks/useAuth'
import { cn } from '@/shared/lib/utils'
type Props = {
@@ -18,6 +20,9 @@ type Props = {
}
export const ProfileHeaderWeb = ({ data, isAuth, userId, totalCount }: Props) => {
const { t } = useTranslation()
+ const { accessToken } = useAuth()
+
+ const { data: dataUser } = useGetUserNameQuery({ name: data?.userName, accessToken })
return (
@@ -31,6 +36,7 @@ export const ProfileHeaderWeb = ({ data, isAuth, userId, totalCount }: Props) =>
alt={''}
width={204}
height={204}
+ priority
/>
) : (
@@ -56,7 +62,7 @@ export const ProfileHeaderWeb = ({ data, isAuth, userId, totalCount }: Props) =>
- 87
+ {dataUser?.followingCount}
@@ -70,7 +76,7 @@ export const ProfileHeaderWeb = ({ data, isAuth, userId, totalCount }: Props) =>
- 112
+ {dataUser?.followersCount}
diff --git a/src/widgets/profileSettings/account-management/AccountManagement.module.scss b/src/widgets/profileSettings/account-management/AccountManagement.module.scss
deleted file mode 100644
index 122e27e2..00000000
--- a/src/widgets/profileSettings/account-management/AccountManagement.module.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.styles {
- display: flex;
-}
diff --git a/src/widgets/profileSettings/account-management/AccountManagement.tsx b/src/widgets/profileSettings/account-management/AccountManagement.tsx
deleted file mode 100644
index b15798c3..00000000
--- a/src/widgets/profileSettings/account-management/AccountManagement.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { TabsLayout } from '@/widgets/layouts'
-
-const Component = () =>
Account management
-
-export const AccountManagement = () => {
- return (
-
-
-
- )
-}
diff --git a/src/widgets/profileSettings/account-management/index.ts b/src/widgets/profileSettings/account-management/index.ts
new file mode 100644
index 00000000..ecba3971
--- /dev/null
+++ b/src/widgets/profileSettings/account-management/index.ts
@@ -0,0 +1 @@
+export { AccountManagement } from './ui/AccountManagement'
diff --git a/src/widgets/profileSettings/account-management/ui/AccountManagement.module.scss b/src/widgets/profileSettings/account-management/ui/AccountManagement.module.scss
new file mode 100644
index 00000000..f2691bd2
--- /dev/null
+++ b/src/widgets/profileSettings/account-management/ui/AccountManagement.module.scss
@@ -0,0 +1,64 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+}
+
+.wrapper {
+ margin-top: 10px;
+ padding: 24px 0 0 20px;
+ background-color: var(--color-dark-500);
+ border: 1px solid var(--color-dark-300);
+}
+
+.wrapperWithFlex {
+ display: flex;
+ gap: 50px;
+ justify-content: left;
+ padding-bottom: 24px;
+}
+
+.time {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.colorText {
+ color: var(--color-light-900);
+}
+
+.checkbox {
+ margin-top: 10px;
+ margin-left: -0.6rem;
+}
+
+.businessContainer {
+ position: relative;
+}
+
+.payPalAndStripe {
+ position: absolute;
+ right: 0;
+
+ display: flex;
+ gap: 50px;
+ align-items: center;
+
+ margin-top: 20px;
+
+ button {
+ padding: 0;
+ }
+}
+
+.payPal {
+ padding: 10px;
+ background-color: var(--color-dark-500);
+ border: 1px solid var(--color-dark-300);
+ border-radius: 7px;
+}
+
+.successButton {
+ margin-top: 40px;
+}
diff --git a/src/widgets/profileSettings/account-management/ui/AccountManagement.tsx b/src/widgets/profileSettings/account-management/ui/AccountManagement.tsx
new file mode 100644
index 00000000..6328df74
--- /dev/null
+++ b/src/widgets/profileSettings/account-management/ui/AccountManagement.tsx
@@ -0,0 +1,163 @@
+import React, { useEffect, useState } from 'react'
+
+import { useRouter } from 'next/router'
+
+import styles from './AccountManagement.module.scss'
+import { setLocalStorageAndValue } from './setLocalStorageAndValue'
+import { StatusModal } from './StatusModal'
+
+import { useSubscribeMutation } from '@/entities/subscription'
+import {
+ useCurrentSubscriptionQuery,
+ useGetPaymentsQuery,
+} from '@/entities/subscription/api/subscriptionApi'
+import { Typography } from '@/shared/components'
+import { RadioGr } from '@/shared/components/radio-group'
+import { useFetchLoader, useTranslation } from '@/shared/lib'
+import { useAuth } from '@/shared/lib/hooks/useAuth'
+import { addLangValue } from '@/shared/lib/utils/addLangValue'
+import { ISubscriptionBody } from '@/shared/types'
+import { TabsLayout } from '@/widgets/layouts'
+import { BusinessType } from '@/widgets/profileSettings/account-management/ui/BusinessType'
+import { InfoPanel } from '@/widgets/profileSettings/account-management/ui/InfoPanel'
+
+const valueLS = {
+ price: 'price' as PriceType,
+ type: 'type' as PriceType,
+}
+
+const Component = () => {
+ const { t } = useTranslation()
+ const { accessToken } = useAuth()
+ const router = useRouter()
+
+ const [valuePrice, setValuePrice] = useState
(() => {
+ return (localStorage.getItem(valueLS.price) || t.subscription.day) as ValuePriceType
+ })
+ const [valueType, setValueType] = useState(() => {
+ return (localStorage.getItem(valueLS.type) || t.account_type.personal) as ValueType
+ })
+ const [openModal, setOpenModal] = useState(false)
+
+ const [subscribe, { isLoading, isError }] = useSubscribeMutation()
+ const { data: curData } = useCurrentSubscriptionQuery(accessToken)
+ const { data: payments, isLoading: isLoadPayments } = useGetPaymentsQuery(accessToken)
+
+ const typeAccount = [
+ { label: t.account_type.personal, value: t.account_type.personal },
+ { label: t.account_type.business, value: t.account_type.business },
+ ]
+
+ useFetchLoader(isLoading)
+ useFetchLoader(isLoadPayments)
+
+ let detectionEndDay
+ let nextDay
+
+ if (payments?.length > 0) {
+ const endDate = new Date(payments[payments.length - 1].endDateOfSubscription)
+
+ detectionEndDay = endDate.toLocaleDateString('ru-RU')
+ nextDay = new Date(endDate.setDate(endDate.getDate() + 1)).toLocaleDateString('ru-RU')
+ }
+
+ const addSubscribe = async (body: ISubscriptionBody) => {
+ const result = await subscribe({
+ body,
+ accessToken,
+ }).unwrap()
+
+ await router.push(result.url)
+ }
+
+ const onSuccess = () => {
+ if (isError) return
+
+ setOpenModal(false)
+ }
+
+ const onChangTypeAccount = (value: ValueType) => {
+ setValueType(value)
+ localStorage.setItem(valueLS.type, value)
+ localStorage.setItem(valueLS.price, t.subscription.day)
+ }
+
+ useEffect(() => {
+ if (router.query.success) {
+ setOpenModal(true)
+ }
+ }, [router.query.success])
+
+ useEffect(() => {
+ setValueType(addLangValue(t.lg as LangType, valueType))
+ setValuePrice(addLangValue(t.lg as LangType, valuePrice))
+ }, [router.locale, t.lg, valuePrice, valueType])
+
+ useEffect(() => {
+ if (curData?.data.length > 0) {
+ setLocalStorageAndValue(t.account_type.business as ValueType, valueLS.type, setValueType)
+ } else {
+ setLocalStorageAndValue(t.account_type.personal as ValueType, valueLS.type, setValueType)
+ }
+ }, [curData, t.account_type.business, t.account_type.personal])
+
+ return (
+
+ {curData?.data.length > 0 && (
+
+ )}
+ {!isLoadPayments && (
+
+
{t.text_account}:
+
+ onChangTypeAccount(value as ValueType)}
+ options={typeAccount}
+ value={valueType}
+ />
+
+
+ )}
+
+ {valueType === t.account_type.business && (
+
+ )}
+ {router.query.success && openModal ? (
+
+ ) : null}
+ {isError && router.query.success && openModal ? (
+
+ ) : null}
+
+ )
+}
+
+export const AccountManagement = () => {
+ return (
+
+
+
+ )
+}
diff --git a/src/widgets/profileSettings/account-management/ui/BusinessType.tsx b/src/widgets/profileSettings/account-management/ui/BusinessType.tsx
new file mode 100644
index 00000000..a74c3db2
--- /dev/null
+++ b/src/widgets/profileSettings/account-management/ui/BusinessType.tsx
@@ -0,0 +1,66 @@
+import React, { MouseEventHandler } from 'react'
+
+import { PayPal, Stripe } from '@/shared/assets'
+import { Button, Typography } from '@/shared/components'
+import { RadioGr } from '@/shared/components/radio-group'
+import { LangType } from '@/shared/locales/en'
+import { ISubscriptionBody } from '@/shared/types'
+import styles from '@/widgets/profileSettings/account-management/ui/AccountManagement.module.scss'
+
+type Props = {
+ t: LangType
+ setValuePrice: (value: ValuePriceType) => void
+ valuePrice: ValuePriceType
+ addSubscribe: (body: ISubscriptionBody) => void
+}
+
+export const BusinessType = ({ t, valuePrice, addSubscribe, setValuePrice }: Props) => {
+ const businessPrice = [
+ { label: t.subscription.day, value: t.subscription.day },
+ { label: t.subscription.week, value: t.subscription.week },
+ { label: t.subscription.month, value: t.subscription.month },
+ ]
+ const data: DataType = {
+ [t.subscription.day]: { amount: '10', period: 'DAY' },
+ [t.subscription.week]: { amount: '50', period: 'WEEKLY' },
+ [t.subscription.month]: { amount: '100', period: 'MONTHLY' },
+ }
+
+ const handlerSubscribe: MouseEventHandler = e => {
+ const body: ISubscriptionBody = {
+ typeSubscription: data[valuePrice as ValuePriceType].period,
+ paymentType: e.currentTarget.name.toUpperCase(),
+ amount: Number(data[valuePrice as ValuePriceType].amount),
+ baseUrl: window.location.href,
+ }
+
+ addSubscribe(body)
+ }
+
+ const onChangPrice = (value: ValuePriceType) => {
+ localStorage.setItem('price', value)
+ setValuePrice(value)
+ }
+
+ return (
+
+
{t.text_subscription_costs}:
+
+ onChangPrice(value as ValuePriceType)}
+ options={businessPrice}
+ value={valuePrice}
+ />
+
+
+
+ )
+}
diff --git a/src/widgets/profileSettings/account-management/ui/InfoPanel.tsx b/src/widgets/profileSettings/account-management/ui/InfoPanel.tsx
new file mode 100644
index 00000000..fc5e9d2e
--- /dev/null
+++ b/src/widgets/profileSettings/account-management/ui/InfoPanel.tsx
@@ -0,0 +1,52 @@
+import React, { useState } from 'react'
+
+import { useAutoRenewalMutation } from '@/entities/subscription/api/subscriptionApi'
+import { SuperCheckbox, Typography } from '@/shared/components'
+import { useAuth } from '@/shared/lib/hooks/useAuth'
+import { LangType } from '@/shared/locales/en'
+import styles from '@/widgets/profileSettings/account-management/ui/AccountManagement.module.scss'
+
+type Props = {
+ t: LangType
+ detectionEndDay: string | undefined
+ nextDay: string | undefined
+ hasAutoRenewal: boolean | undefined
+}
+
+export const InfoPanel = ({ t, detectionEndDay, nextDay, hasAutoRenewal }: Props) => {
+ const { accessToken } = useAuth()
+
+ const [isChecked, setChecked] = useState(hasAutoRenewal ?? true)
+ const [autoRenewal] = useAutoRenewalMutation()
+
+ const onCheckbox = () => {
+ autoRenewal(accessToken)
+ setChecked(!isChecked)
+ }
+
+ return (
+
+
{t.current_subscription}:
+
+
+ {t.expire_at}
+ {detectionEndDay}
+
+
+ {isChecked && (
+ <>
+ {t.next_payment}
+ {nextDay}
+ >
+ )}
+
+
+
+
+ )
+}
diff --git a/src/widgets/profileSettings/account-management/ui/StatusModal.tsx b/src/widgets/profileSettings/account-management/ui/StatusModal.tsx
new file mode 100644
index 00000000..36e05300
--- /dev/null
+++ b/src/widgets/profileSettings/account-management/ui/StatusModal.tsx
@@ -0,0 +1,30 @@
+import React from 'react'
+
+import styles from './AccountManagement.module.scss'
+
+import { Button, Typography } from '@/shared/components'
+import { Modal } from '@/shared/components/modals'
+
+type Props = {
+ openModal: boolean
+ titleModal: string
+ textTypography: string
+ callback: () => void
+ textButton: string
+}
+export const StatusModal = ({
+ openModal,
+ titleModal,
+ textTypography,
+ callback,
+ textButton,
+}: Props) => {
+ return (
+
+ {textTypography}
+
+ {textButton}
+
+
+ )
+}
diff --git a/src/widgets/profileSettings/account-management/ui/setLocalStorageAndValue.ts b/src/widgets/profileSettings/account-management/ui/setLocalStorageAndValue.ts
new file mode 100644
index 00000000..1a33cc2b
--- /dev/null
+++ b/src/widgets/profileSettings/account-management/ui/setLocalStorageAndValue.ts
@@ -0,0 +1,8 @@
+export const setLocalStorageAndValue = (
+ type: ValueType,
+ value: PriceType,
+ setValueType: (value: ValueType) => void
+) => {
+ localStorage.setItem(value, type)
+ setValueType(type)
+}
diff --git a/src/widgets/profileSettings/devices/Devices.module.scss b/src/widgets/profileSettings/devices/Devices.module.scss
deleted file mode 100644
index 122e27e2..00000000
--- a/src/widgets/profileSettings/devices/Devices.module.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.styles {
- display: flex;
-}
diff --git a/src/widgets/profileSettings/devices/Devices.tsx b/src/widgets/profileSettings/devices/Devices.tsx
deleted file mode 100644
index 69f3b1ae..00000000
--- a/src/widgets/profileSettings/devices/Devices.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { TabsLayout } from '@/widgets/layouts'
-
-const Component = () => Devices
-
-export const Devices = () => {
- return (
-
-
-
- )
-}
diff --git a/src/widgets/profileSettings/devices/index.ts b/src/widgets/profileSettings/devices/index.ts
new file mode 100644
index 00000000..63b9ea15
--- /dev/null
+++ b/src/widgets/profileSettings/devices/index.ts
@@ -0,0 +1 @@
+export { Devices } from './ui/Devices'
diff --git a/src/widgets/profileSettings/devices/ui/CardsDevice/CardsDevice.module.scss b/src/widgets/profileSettings/devices/ui/CardsDevice/CardsDevice.module.scss
new file mode 100644
index 00000000..b88c084e
--- /dev/null
+++ b/src/widgets/profileSettings/devices/ui/CardsDevice/CardsDevice.module.scss
@@ -0,0 +1,92 @@
+.cardsDevice {
+ display: flex;
+ align-items: center;
+
+ width: 100%;
+ height: 7.5rem;
+ margin-bottom: 0.6rem;
+
+ background: var(--color-dark-500);
+ border: 1px solid var(--color-dark-300);
+
+ .icon {
+ display: flex;
+
+ width: 3.6rem;
+ height: 3.6rem;
+ margin-left: 0.8rem;
+ padding: 0.1rem;
+
+ @media (width <= 768px) {
+ width: 10.5rem;
+ }
+ }
+
+ .details {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ width: 20rem;
+ height: 5rem;
+ margin-left: 0.2rem;
+ padding: 0.5rem;
+
+ .name {
+ margin-bottom: 0.4rem;
+ }
+
+ @media (width <= 768px) {
+ height: auto;
+ }
+ }
+
+ .button {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+ justify-content: flex-end;
+
+ margin-left: 45rem;
+ padding-right: 2rem;
+
+ font-size: 14px;
+ font-weight: 500;
+ font-style: normal;
+ line-height: 24px;
+ color: #fff;
+ text-decoration: none;
+
+ &:active {
+ color: var(--color-accent-500);
+ fill: var(--color-accent-500);
+ stroke: var(--color-accent-500);
+ }
+
+ &:hover {
+ cursor: pointer;
+ color: var(--color-accent-100);
+ stroke: var(--color-accent-100);
+ }
+
+ &:focus-visible {
+ border-radius: 2px;
+ outline: 2px solid var(--color-accent-700);
+ }
+
+ &:disabled {
+ color: var(--color-dark-100);
+ stroke: var(--color-dark-100);
+ }
+
+ @media (width <= 768px) {
+ margin-left: auto;
+ }
+ }
+
+ @media (width <= 768px) {
+ width: 24rem;
+ height: auto;
+ padding: 1rem;
+ }
+}
diff --git a/src/widgets/profileSettings/devices/ui/CardsDevice/CardsDevice.tsx b/src/widgets/profileSettings/devices/ui/CardsDevice/CardsDevice.tsx
new file mode 100644
index 00000000..3c91bd80
--- /dev/null
+++ b/src/widgets/profileSettings/devices/ui/CardsDevice/CardsDevice.tsx
@@ -0,0 +1,70 @@
+import React, { ReactNode } from 'react'
+
+import { format } from 'date-fns'
+import { LogOut } from 'lucide-react'
+
+import s from './CardsDevice.module.scss'
+
+import { LogOutIcon } from '@/shared/assets'
+import { Typography } from '@/shared/components'
+import { useTranslation } from '@/shared/lib'
+import { LogOutButton } from '@/widgets/logOut'
+
+type Props = {
+ icon: ReactNode
+ deviceName: string
+ IP: string
+ visited?: Date
+ deviceId?: number
+ handleDeleteSession?: (deviceId: number) => void
+}
+
+export const CardsCurrentDevice = ({ IP, deviceName, icon, visited }: Props) => {
+ return (
+
+
{icon}
+
+
+ {deviceName}
+
+
+ IP:{IP}
+
+
+
+ )
+}
+
+export const CardsActiveDevice = ({
+ IP,
+ deviceName,
+ icon,
+ visited,
+ handleDeleteSession,
+ deviceId,
+}: Props) => {
+ const lastActiveDate = visited ? new Date(visited) : null
+ const formattedDate = lastActiveDate ? format(lastActiveDate, 'yyyy-MM-dd') : ''
+ const { t } = useTranslation()
+ const onClikHandler = () => {
+ handleDeleteSession && handleDeleteSession(deviceId!)
+ }
+
+ return (
+
+
{icon}
+
+
+ {deviceName}
+
+
+ IP: {IP}
+
+ Last Active: {formattedDate}
+
+
+ {t.sidebar.log_out}
+
+
+ )
+}
diff --git a/src/widgets/profileSettings/devices/ui/Devices.module.scss b/src/widgets/profileSettings/devices/ui/Devices.module.scss
new file mode 100644
index 00000000..bfe08b39
--- /dev/null
+++ b/src/widgets/profileSettings/devices/ui/Devices.module.scss
@@ -0,0 +1,21 @@
+.styles {
+ display: flex;
+}
+
+.spacer {
+ margin-bottom: 3rem;
+}
+
+.button {
+ display: flex;
+ justify-content: flex-end;
+ padding-top: 1rem;
+
+ @media (width <= 768px) {
+ justify-content: center;
+ }
+}
+
+.typorhy {
+ padding-bottom: 10px;
+}
diff --git a/src/widgets/profileSettings/devices/ui/Devices.tsx b/src/widgets/profileSettings/devices/ui/Devices.tsx
new file mode 100644
index 00000000..ce24ac75
--- /dev/null
+++ b/src/widgets/profileSettings/devices/ui/Devices.tsx
@@ -0,0 +1,115 @@
+import React, { useEffect, useState } from 'react'
+
+import s from './Devices.module.scss'
+
+import {
+ useDeleteAllMutation,
+ useDeleteSessionMutation,
+ useGetDevicesQuery,
+} from "@/entities/device's"
+import { ChromeIcon } from '@/shared/assets/icons/ChromeIcon'
+import { MackIcon } from '@/shared/assets/icons/MackIcon'
+import { PhoneIcon } from '@/shared/assets/icons/PhoneIcon'
+import { Button, Typography } from '@/shared/components'
+import { useErrorHandler, useFetchLoader, useTranslation } from '@/shared/lib'
+import { useAuth } from '@/shared/lib/hooks/useAuth'
+import { TabsLayout } from '@/widgets/layouts'
+import {
+ CardsActiveDevice,
+ CardsCurrentDevice,
+} from '@/widgets/profileSettings/devices/ui/CardsDevice/CardsDevice'
+
+const Component = () => {
+ const { t } = useTranslation()
+
+ const { accessToken } = useAuth()
+ const { data, isLoading, error } = useGetDevicesQuery({ accessToken })
+ const [deleteDevice, { isLoading: deleteLoadingAll, error: deleteErrorAll }] =
+ useDeleteAllMutation()
+
+ const [deleteSessionDevice, { isLoading: deleteLoading, error: deleteError }] =
+ useDeleteSessionMutation()
+ const [sortedDevices, setSortedDevices] = useState([])
+
+ useEffect(() => {
+ if (data && data.length > 0) {
+ const parsedData = data.map((item: Device) => ({
+ ...item,
+ }))
+ const sorted = parsedData.sort((a, b) => {
+ return a.deviceId - b.deviceId
+ })
+
+ setSortedDevices(sorted)
+ }
+ }, [data])
+
+ useFetchLoader(isLoading || deleteLoading || deleteLoadingAll)
+
+ useErrorHandler((deleteError || deleteErrorAll) as CustomerError)
+ const onClickHandler = () => {
+ deleteDevice({ accessToken })
+ }
+
+ const handleDeleteSession = (deviceId: number) => {
+ data && deleteSessionDevice({ deviceId, accessToken })
+ }
+
+ return (
+
+ {sortedDevices.length > 0 && (
+ <>
+
+ Current Device
+
+
:
}
+ IP={sortedDevices[0].ip}
+ deviceName={sortedDevices[0].osName}
+ />
+
+
+ {t.devices.Terminate_sessions}
+
+
+ >
+ )}
+
+ {sortedDevices.length > 0 && (
+ <>
+ {sortedDevices.slice(1).map(device => (
+
+
+ Active Devices
+
+ : }
+ deviceName={device.osName}
+ IP={device.ip}
+ deviceId={device.deviceId}
+ handleDeleteSession={handleDeleteSession}
+ />
+
+ ))}
+ >
+ )}
+
+
+
+ )
+}
+
+export const Devices = () => {
+ return (
+
+
+
+ )
+}
diff --git a/src/widgets/profileSettings/generalInformation/ui/GeneralInformation.tsx b/src/widgets/profileSettings/generalInformation/ui/GeneralInformation.tsx
index d7488414..0ceaf0f1 100644
--- a/src/widgets/profileSettings/generalInformation/ui/GeneralInformation.tsx
+++ b/src/widgets/profileSettings/generalInformation/ui/GeneralInformation.tsx
@@ -92,6 +92,10 @@ const Information = () => {
body.aboutMe = ' '
}
+ if (!body.dateOfBirth) {
+ body.dateOfBirth = profile?.dateOfBirth
+ }
+
putProfile({
body,
accessToken,
@@ -100,7 +104,6 @@ const Information = () => {
useEffect(() => {
if (profile) {
- trigger()
setTimeout(() => {
date && handleDate(date)
})
@@ -113,11 +116,19 @@ const Information = () => {
profile?.lastName && setValue('lastName', profile.lastName)
profile?.userName && setValue('userName', profile.userName)
profile?.aboutMe && setValue('aboutMe', profile.aboutMe)
+ profile?.aboutMe && setValue('dateOfBirth', profile.dateOfBirth)
// eslint-disable-next-line react-hooks/exhaustive-deps, prettier/prettier
- }, [profile?.firstName, profile?.lastName, profile?.userName, profile?.aboutMe])
+ }, [
+ profile?.firstName,
+ profile?.lastName,
+ profile?.userName,
+ profile?.aboutMe,
+ profile?.dateOfBirth,
+ ])
useEffect(() => {
+ trigger()
isSuccess && dispatch(setAlert({ message: t.profile.success, variant: 'info' }))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess])
diff --git a/src/widgets/profileSettings/index.ts b/src/widgets/profileSettings/index.ts
index b610208c..8a3ab44a 100644
--- a/src/widgets/profileSettings/index.ts
+++ b/src/widgets/profileSettings/index.ts
@@ -1,4 +1,4 @@
export { GeneralInformation } from './generalInformation/ui/GeneralInformation'
-export { Devices } from './devices/Devices'
+export { Devices } from '@/widgets/profileSettings/devices/ui/Devices'
export { MyPayments } from './my-payments/MyPayments'
-export { AccountManagement } from './account-management/AccountManagement'
+export { AccountManagement } from './account-management/ui/AccountManagement'
diff --git a/src/widgets/profileSettings/my-payments/MyPayments.module.scss b/src/widgets/profileSettings/my-payments/MyPayments.module.scss
index 122e27e2..bc7e189f 100644
--- a/src/widgets/profileSettings/my-payments/MyPayments.module.scss
+++ b/src/widgets/profileSettings/my-payments/MyPayments.module.scss
@@ -1,3 +1,5 @@
-.styles {
- display: flex;
+@use 'src/shared/assets/mixins';
+
+.table {
+ @include mixins.table_styles;
}
diff --git a/src/widgets/profileSettings/my-payments/MyPayments.tsx b/src/widgets/profileSettings/my-payments/MyPayments.tsx
index 713ddd1d..8e185c9d 100644
--- a/src/widgets/profileSettings/my-payments/MyPayments.tsx
+++ b/src/widgets/profileSettings/my-payments/MyPayments.tsx
@@ -1,6 +1,87 @@
+import { useEffect, useState } from 'react'
+
+import styles from './MyPayments.module.scss'
+import { getPageItems } from './utils/getPageItems'
+
+import { useGetPaymentsQuery } from '@/entities/subscription/api/subscriptionApi'
+import { Pagination } from '@/shared/components'
+import { useFetchLoader, useTranslation } from '@/shared/lib'
+import { useAuth } from '@/shared/lib/hooks/useAuth'
+import { IPayments } from '@/shared/types'
import { TabsLayout } from '@/widgets/layouts'
-const Component = () => My payments
+const Component = () => {
+ const { t } = useTranslation()
+ const { accessToken } = useAuth()
+
+ const [currentPage, setCurrentPage] = useState(1)
+ const [pageSize, setPageSize] = useState(10)
+ const [array, setArray] = useState([])
+
+ const { data: payments, isLoading: isLoadPayments } = useGetPaymentsQuery(accessToken)
+
+ useFetchLoader(isLoadPayments)
+
+ const hashTabType: { [key: string]: string } = {
+ DAY: '1 day',
+ WEEKLY: '7 days',
+ MONTHLY: '1 month',
+ STRIPE: 'Stripe',
+ PAYPAL: 'PayPal',
+ }
+
+ const onCurrentPageChange = (value: number | string) => {
+ setCurrentPage(value)
+ }
+
+ const onPageSizeChange = (value: number) => {
+ setPageSize(value)
+ }
+
+ useEffect(() => {
+ setArray(getPageItems(currentPage as number, pageSize, payments || []))
+ }, [payments, currentPage, pageSize])
+
+ return (
+ !isLoadPayments && (
+
+
+
+
+ {t.date_of_payment}
+ {t.end_date_of_subscription}
+ {t.price}
+ {t.subscription_type}
+ {t.payment_type}
+
+ {array.map((item: IPayments) => (
+
+ {new Date(item.dateOfPayment).toLocaleDateString('ru-RU')}
+ {new Date(item.endDateOfSubscription).toLocaleDateString('ru-RU')}
+ ${item.price}
+ {hashTabType[item.subscriptionType]}
+ {hashTabType[item.paymentType]}
+
+ ))}
+
+
+
+
+ )
+ )
+}
export const MyPayments = () => {
return (
diff --git a/src/widgets/profileSettings/my-payments/utils/getPageItems.ts b/src/widgets/profileSettings/my-payments/utils/getPageItems.ts
new file mode 100644
index 00000000..f2cdd8df
--- /dev/null
+++ b/src/widgets/profileSettings/my-payments/utils/getPageItems.ts
@@ -0,0 +1,6 @@
+export function getPageItems(currentPage: number, itemsPerPage: number, array: T[]) {
+ const startIndex = (currentPage - 1) * itemsPerPage
+ const endIndex = startIndex + itemsPerPage
+
+ return array.slice(startIndex, endIndex)
+}
diff --git a/src/widgets/search/ui/Search.module.scss b/src/widgets/search/ui/Search.module.scss
new file mode 100644
index 00000000..ad8a4cc4
--- /dev/null
+++ b/src/widgets/search/ui/Search.module.scss
@@ -0,0 +1,3 @@
+.userName {
+ color: var(--color-light-900);
+}
\ No newline at end of file
diff --git a/src/widgets/search/ui/SearchUser.tsx b/src/widgets/search/ui/SearchUser.tsx
new file mode 100644
index 00000000..11751f50
--- /dev/null
+++ b/src/widgets/search/ui/SearchUser.tsx
@@ -0,0 +1,34 @@
+import { useEffect, useState } from 'react'
+
+import { useGetUsersNameQuery } from '@/entities/users-follow/api/usersFollowApi'
+import { useFetchLoader } from '@/shared/lib'
+import { useAuth } from '@/shared/lib/hooks/useAuth'
+import { SearchedUsers } from '@/widgets/search/ui/SearchedUsers'
+import { DebouncedInput } from '@/widgets/superAdmin/userList/DebouncedInput'
+
+export const SearchUser = () => {
+ const { accessToken } = useAuth()
+
+ const [valueSearch, setValueSearch] = useState('')
+ const [userDetail, setUserDetail] = useState([])
+
+ const { data: dataUsers, isLoading } = useGetUsersNameQuery({
+ name: valueSearch ? valueSearch : null,
+ accessToken,
+ })
+
+ useEffect(() => {
+ if (!dataUsers) return
+
+ setUserDetail(dataUsers.items)
+ }, [valueSearch, dataUsers])
+
+ useFetchLoader(isLoading)
+
+ return (
+
+
+
+
+ )
+}
diff --git a/src/widgets/search/ui/SearchedUsers.tsx b/src/widgets/search/ui/SearchedUsers.tsx
new file mode 100644
index 00000000..487f0502
--- /dev/null
+++ b/src/widgets/search/ui/SearchedUsers.tsx
@@ -0,0 +1,31 @@
+import Link from 'next/link'
+
+import s from './Search.module.scss'
+
+import { Typography } from '@/shared/components'
+import { AvatarSmallView } from '@/shared/components/avatarSmallView'
+
+type Props = {
+ users: SearchUsersItems[]
+}
+
+export const SearchedUsers = ({ users }: Props) => {
+ return (
+
+ {users.map(user => (
+
+
+
+
+ {user.userName}
+
+
+ {user.firstName ? `${user.firstName} ` : '---- '}
+ {user.lastName ? user.lastName : '----'}
+
+
+
+ ))}
+
+ )
+}
diff --git a/src/widgets/search/ui/userProfile/UserProfile.tsx b/src/widgets/search/ui/userProfile/UserProfile.tsx
new file mode 100644
index 00000000..13bec3de
--- /dev/null
+++ b/src/widgets/search/ui/userProfile/UserProfile.tsx
@@ -0,0 +1,89 @@
+import { useEffect, useState } from 'react'
+
+import { useGetPostsByUserMutation } from '@/entities/users/api/usersApi'
+import {
+ useFollowingMutation,
+ useGetUserNameQuery,
+ useUnFollowingMutation,
+} from '@/entities/users-follow/api/usersFollowApi'
+import { Button, Typography } from '@/shared/components'
+import { AvatarSmallView } from '@/shared/components/avatarSmallView'
+import { useFetchLoader, useTranslation } from '@/shared/lib'
+import { useAuth } from '@/shared/lib/hooks/useAuth'
+import { UploadedPhotos } from '@/widgets/superAdmin/userList/moreInformation/table-info/photos/UploadedPhotos'
+
+type Props = {
+ userName: string
+}
+
+export const UserProfile = ({ userName }: Props) => {
+ const { t } = useTranslation()
+ const { accessToken } = useAuth()
+
+ const [posts, setPosts] = useState(null)
+
+ const { data: dataUser, refetch } = useGetUserNameQuery({ name: userName, accessToken })
+ const [getPosts] = useGetPostsByUserMutation()
+ const [following, { isLoading: loadingFollowing }] = useFollowingMutation()
+ const [unFollowing, { isLoading: loadingUnFollowing }] = useUnFollowingMutation()
+
+ useEffect(() => {
+ if (!dataUser) return
+ getPosts({ id: dataUser?.id, endCursorId: 0 })
+ .unwrap()
+ .then(res => {
+ setPosts(res.data.getPostsByUser)
+ })
+ }, [dataUser])
+
+ const getFollow = () => {
+ dataUser?.isFollowing
+ ? unFollowing({ userId: dataUser?.id, accessToken })
+ : following({ userId: dataUser?.id, accessToken })
+ refetch()
+ }
+
+ useFetchLoader(loadingFollowing || loadingUnFollowing)
+
+ return (
+ <>
+
+
+
+
+
{dataUser?.userName}
+
+
+ {dataUser?.isFollowing
+ ? t.delete_following.delete_button
+ : t.following_modal.follow_button}
+
+ {}}>
+ {t.sendMessage}
+
+
+
+
+
+ {dataUser?.followingCount}
+
+ {t.following_modal.followings_title}
+
+
+ {dataUser?.followersCount}
+
+ {t.followers_modal.modals_title}
+
+
+ {dataUser?.publicationsCount}
+
+ {t.publications}
+
+
+
{dataUser?.aboutMe}
+
+
+
+ >
+ )
+}
diff --git a/src/widgets/signIn/signInAuth/SignInAuth.tsx b/src/widgets/signIn/signInAuth/SignInAuth.tsx
index df44c901..f55c380d 100644
--- a/src/widgets/signIn/signInAuth/SignInAuth.tsx
+++ b/src/widgets/signIn/signInAuth/SignInAuth.tsx
@@ -7,6 +7,19 @@ import { IAuthFields } from '@/shared/types'
export const SignInAuth: FC = ({ register, formState: { errors } }) => {
const { t } = useTranslation()
+ const validatePassword = (value: any) => {
+ if (value.includes('admin')) {
+ return true
+ }
+ if (value.length < 6) {
+ return t.messages.password_min_length
+ }
+ if (!passwordValidation.test(value)) {
+ return t.messages.password_validate_message
+ }
+
+ return true
+ }
return (
<>
@@ -26,18 +39,19 @@ export const SignInAuth: FC = ({ register, formState: { errors } })
{
const { isClient } = useClient()
const { t } = useTranslation()
const [Login, { isLoading, error, isSuccess }] = useLoginMutation()
-
+ const [loginAdminMutation, { isSuccess: isSuccessAdmin, isLoading: isLoadingAdmin, data }] =
+ useLoginAdminMutation()
+ const dispatch = useAppDispatch()
const router = useRouter()
const onSubmit: SubmitHandler = data => {
+ loginAdminMutation({ email: data.email, password: data.password })
Login({ email: data.email, password: data.password })
}
@@ -45,6 +52,20 @@ export const SignInWidget: FC = () => {
window.location.assign(url)
}
+ useEffect(() => {
+ if (!router.query.superAdmin) {
+ localStorage.removeItem('isAdmin')
+ }
+ }, [router.query])
+ useEffect(() => {
+ if (data?.data?.loginAdmin?.logged) {
+ localStorage.setItem('isAdmin', JSON.stringify(true))
+ dispatch(adminSlice.actions.isAdmin(true))
+ router.push('/superAdmin')
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [data, dispatch, router])
+
useEffect(() => {
isSuccess && router.push('/my-profile')
}, [isSuccess])
@@ -61,7 +82,7 @@ export const SignInWidget: FC = () => {
isClient && trigger()
}, [t.signin.error_message])
- useFetchLoader(isLoading || socialsLoading)
+ useFetchLoader(isLoading || socialsLoading || isLoadingAdmin)
return (
@@ -91,6 +112,7 @@ export const SignInWidget: FC = () => {
{t.signin.sign_in}
{t.signin.account_question}
+
{
+ const { t } = useTranslation()
+ const [searchUserName, setSearchUserName] = useState
('')
+ const [pagedData, setPagedData] = useState(null)
+
+ const [getPayments, { isLoading }] = useGetPaymentsLIstMutation()
+ const { pageSize, setPageSize, setCurrentPage, currentPage, sortBy, setSortBy } = usePagination()
+ const { icon, onSortChange, sort } = useSortBy()
+
+ useEffect(() => {
+ const initialObject = {
+ pageSize,
+ pageNumber: currentPage as number,
+ sortBy,
+ sortDirection: sort === 'default' ? SortDirection.DESC : (sort as SortDirection),
+ searchTerm: searchUserName,
+ }
+
+ getPayments(initialObject)
+ .unwrap()
+ .then(res => {
+ if (JSON.stringify(res.data.getPayments) !== JSON.stringify(pagedData)) {
+ setPagedData(res.data.getPayments)
+ }
+ })
+ .catch(er => console.error(er))
+ }, [currentPage, pageSize, searchUserName, sortBy, sort])
+
+ const onDebounce = (value: string) => {
+ setSearchUserName(value)
+ }
+
+ const onChangeSortBy = (e: React.MouseEvent, key: string) => {
+ setSortBy(`${e.currentTarget.innerText}`)
+ onSortChange(key)
+ }
+
+ useFetchLoader(isLoading)
+
+ return (
+
+
+
+
+ {pagedData?.items.length ? (
+ <>
+
+
+
+ onChangeSortBy(e, 'name')}>
+ {t.user_list.name}
+ {icon('name')}
+
+ onChangeSortBy(e, 'date')}>
+ {t.user_list.date}
+ {icon('date')}
+
+ onChangeSortBy(e, 'amount')}>
+ {t.amount}, ${icon('amount')}
+
+ {t.subscription_text}
+ onChangeSortBy(e, 'method')}>
+ {t.payment_method}
+ {icon('method')}
+
+
+ {pagedData?.items.map(item => (
+
+
+
+ {item.userName}
+
+ {new Date(item.createdAt).toLocaleDateString('ru-RU')}
+ {item.amount}
+ {tabType[item.type]}
+ {tabType[item.paymentMethod]}
+
+ ))}
+
+
+
+ >
+ ) : (
+
+ {t.user_info.not_found}
+
+ )}
+
+ )
+}
diff --git a/src/widgets/superAdmin/postsList/PostsList.module.scss b/src/widgets/superAdmin/postsList/PostsList.module.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/widgets/superAdmin/postsList/PostsList.tsx b/src/widgets/superAdmin/postsList/PostsList.tsx
new file mode 100644
index 00000000..cf89e9b3
--- /dev/null
+++ b/src/widgets/superAdmin/postsList/PostsList.tsx
@@ -0,0 +1,140 @@
+import React, { useEffect, useState } from 'react'
+
+import { useSubscription, gql } from '@apollo/client'
+
+import { useGetPostsListMutation } from '@/entities/users/api/usersApi'
+import { SortDirection } from '@/shared/constants/enum'
+import { useFetchLoader } from '@/shared/lib'
+import Post from '@/widgets/superAdmin/postsList/post/Post'
+import { DebouncedInput } from '@/widgets/superAdmin/userList/DebouncedInput'
+
+const NEW_POST_SUBSCRIPTION = gql`
+ subscription {
+ postAdded {
+ images {
+ id
+ createdAt
+ url
+ width
+ height
+ fileSize
+ }
+ id
+ ownerId
+ description
+ createdAt
+ updatedAt
+ postOwner {
+ id
+ userName
+ firstName
+ lastName
+ avatars {
+ url
+ width
+ height
+ fileSize
+ }
+ }
+ }
+ }
+`
+
+export const PostsList = () => {
+ const [searchUserName, setSearchUserName] = useState('')
+ const [loadedPosts, setLoadedPosts] = useState(null)
+ const [fetching, setFetching] = useState(true)
+
+ const [getPosts, { isLoading }] = useGetPostsListMutation()
+ const { data, loading } = useSubscription(NEW_POST_SUBSCRIPTION, {
+ onData: () => {
+ setLoadedPosts(null)
+ },
+ })
+
+ useEffect(() => {
+ const initialPost = {
+ endCursorPostId: loadedPosts?.items[loadedPosts.items.length - 1].id || null,
+ searchTerm: searchUserName,
+ pageSize: 8,
+ sortBy: 'createdAt',
+ sortDirection: SortDirection.DESC,
+ }
+
+ if (fetching || data) {
+ getPosts(initialPost)
+ .unwrap()
+ .then(res => {
+ setLoadedPosts(prev => {
+ if (prev) {
+ return {
+ pageCount: res.data.getPosts.pageCount,
+ pageSize: res.data.getPosts.pageSize,
+ totalCount: res.data.getPosts.totalCount,
+ items: [...prev.items, ...res.data.getPosts.items],
+ }
+ } else {
+ return {
+ ...res.data.getPosts,
+ }
+ }
+ })
+ })
+ .finally(() => {
+ setFetching(false)
+ })
+ }
+ }, [searchUserName, fetching, data])
+
+ const scrollHandler = (e: any) => {
+ if (!loadedPosts) return
+ if (
+ e.target.scrollHeight - (e.target.scrollTop + window.innerHeight) < 100 &&
+ loadedPosts?.items.length < loadedPosts?.totalCount
+ ) {
+ setFetching(true)
+ }
+ }
+
+ useEffect(() => {
+ window.addEventListener('scroll', scrollHandler, true)
+
+ return () => {
+ window.removeEventListener('scroll', scrollHandler, true)
+ }
+ }, [scrollHandler])
+
+ const onDebounce = (value: string) => {
+ setSearchUserName(value)
+ setLoadedPosts(null)
+ setFetching(true)
+ }
+
+ useFetchLoader(isLoading)
+
+ return (
+
+
+
+
+
+
+ {loadedPosts?.items.map(post => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/widgets/superAdmin/postsList/apolloClient/apolloClient.ts b/src/widgets/superAdmin/postsList/apolloClient/apolloClient.ts
new file mode 100644
index 00000000..9d3cfdff
--- /dev/null
+++ b/src/widgets/superAdmin/postsList/apolloClient/apolloClient.ts
@@ -0,0 +1,31 @@
+import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client'
+import { WebSocketLink } from '@apollo/client/link/ws'
+import { getMainDefinition } from '@apollo/client/utilities'
+
+const httpLink = new HttpLink({
+ uri: 'https://inctagram.work/api/v1/graphql',
+})
+
+const wsLink = new WebSocketLink({
+ uri: `wss://inctagram.work/api/v1/graphql`,
+ options: {
+ reconnect: true,
+ },
+})
+
+const splitLink = split(
+ ({ query }) => {
+ const definition = getMainDefinition(query)
+
+ return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
+ },
+ wsLink,
+ httpLink
+)
+
+const client = new ApolloClient({
+ link: splitLink,
+ cache: new InMemoryCache(),
+})
+
+export default client
diff --git a/src/widgets/superAdmin/postsList/post/ExpandableText.tsx b/src/widgets/superAdmin/postsList/post/ExpandableText.tsx
new file mode 100644
index 00000000..130c08e9
--- /dev/null
+++ b/src/widgets/superAdmin/postsList/post/ExpandableText.tsx
@@ -0,0 +1,44 @@
+import React from 'react'
+
+import { useTranslation } from '@/shared/lib'
+
+const styles = {
+ button: {
+ cursor: 'pointer',
+ color: 'blue',
+ textDecoration: 'underline',
+ },
+ text: {
+ marginRight: '5px',
+ },
+}
+
+type Props = {
+ isExpanded: boolean
+ setIsExpanded: (value: boolean) => void
+ maxLength: number
+ text: string
+}
+
+function ExpandableText({ maxLength, text, isExpanded, setIsExpanded }: Props) {
+ const { t } = useTranslation()
+ const displayText =
+ !isExpanded && text.length > maxLength ? `${text.slice(0, maxLength)}...` : text
+
+ return (
+ <>
+ maxLength ? 'break-word' : undefined }}
+ >
+ {displayText}
+
+ {text.length > maxLength && (
+ setIsExpanded(!isExpanded)} style={styles.button}>
+ {isExpanded ? t.hide : t.show_more}
+
+ )}
+ >
+ )
+}
+
+export default ExpandableText
diff --git a/src/widgets/superAdmin/postsList/post/OwnerPost.tsx b/src/widgets/superAdmin/postsList/post/OwnerPost.tsx
new file mode 100644
index 00000000..5064d303
--- /dev/null
+++ b/src/widgets/superAdmin/postsList/post/OwnerPost.tsx
@@ -0,0 +1,58 @@
+import React, { useState } from 'react'
+
+import { SerializedError } from '@reduxjs/toolkit'
+import { FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
+
+import { BlockIcon } from '@/shared/assets/icons/BlockIcon'
+import { AvatarSmallView } from '@/shared/components/avatarSmallView'
+import { ModalBan } from '@/widgets/superAdmin/userList/banUser/ModalBan'
+import { ShowModalBanType } from '@/widgets/superAdmin/userList/UserList'
+
+type Props = {
+ isLoadingBan: boolean
+ userId: number
+ banUser: ({
+ banReason,
+ userId,
+ }: {
+ banReason: string
+ userId: number
+ }) => Promise<{ data: boolean } | { error: FetchBaseQueryError | SerializedError }>
+ avatar: Avatar
+ userName: string
+}
+
+export const OwnerPost = ({ avatar, userName, isLoadingBan, banUser, userId }: Props) => {
+ const [showModalBan, setShowModalBan] = useState({
+ isShow: false,
+ userId,
+ userName,
+ })
+
+ return (
+
+
+
{
+ setShowModalBan({
+ isShow: true,
+ userId,
+ userName,
+ })
+ }}
+ />
+
+
+ )
+}
diff --git a/src/widgets/superAdmin/postsList/post/Post.module.scss b/src/widgets/superAdmin/postsList/post/Post.module.scss
new file mode 100644
index 00000000..e9312704
--- /dev/null
+++ b/src/widgets/superAdmin/postsList/post/Post.module.scss
@@ -0,0 +1,61 @@
+.container {
+ flex-shrink: 0;
+ width: 24%;
+ height: 440px;
+
+ .wrapper {
+ display: flex;
+ word-wrap: break-word;
+ transition: margin-top 0.3s ease-in-out;
+ }
+
+ .wrapper.expanded {
+ margin-top: -240px;
+ }
+}
+
+.item {
+ cursor: pointer;
+ position: relative;
+ width: 100%;
+}
+
+.sticky {
+ z-index: 1;
+ padding: 0 7px;
+ align-self: flex-end;
+
+ width: 100%;
+ min-height: 150px;
+
+ background-color: var(--color-dark-700);
+}
+
+.timeInfo {
+ color: var(--light-900, #8d9094);
+}
+
+.description {
+ .text {
+ width: 360px;
+
+ font-size: 14px;
+ font-weight: 400;
+ font-style: normal;
+ line-height: 24px;
+ color: var(--light-100, #fff);
+ }
+ margin-top: 5px;
+
+ .toggleButton {
+ cursor: pointer;
+ margin-left: 7px;
+
+ font-size: 14px;
+ font-weight: 400;
+ font-style: normal;
+ line-height: 24px;
+ color: var(--primary-500, #397df6);
+ text-decoration-line: underline;
+ }
+}
diff --git a/src/widgets/superAdmin/postsList/post/Post.tsx b/src/widgets/superAdmin/postsList/post/Post.tsx
new file mode 100644
index 00000000..fc81e021
--- /dev/null
+++ b/src/widgets/superAdmin/postsList/post/Post.tsx
@@ -0,0 +1,104 @@
+import React, { memo, useEffect, useRef, useState } from 'react'
+
+import Image from 'next/image'
+import { Navigation, Pagination, Scrollbar } from 'swiper/modules'
+import { Swiper, SwiperSlide } from 'swiper/react'
+
+import s from './Post.module.scss'
+
+import { useBanUserMutation } from '@/entities/users/api/usersApi'
+import { TimeAgo, Typography } from '@/shared/components'
+import { useTranslation } from '@/shared/lib'
+import ExpandableText from '@/widgets/superAdmin/postsList/post/ExpandableText'
+import { OwnerPost } from '@/widgets/superAdmin/postsList/post/OwnerPost'
+
+type Props = {
+ profileAvatar: Avatar
+ postId: number
+ ownerId: number
+ description: string
+ imagesUrl: ImagePost[]
+ userName: string
+ firstName: string
+ lastName: string
+ updatedAt: string
+}
+
+const Post = ({ ownerId, profileAvatar, imagesUrl, description, userName, updatedAt }: Props) => {
+ const { t } = useTranslation()
+
+ const [isExpanded, setIsExpanded] = useState(false)
+ const menuRef = useRef(null)
+
+ useEffect(() => {
+ const handler = (e: MouseEvent): void => {
+ !menuRef.current?.contains(e.target as Node) && setIsExpanded(false)
+ }
+
+ document.addEventListener('mousedown', handler)
+
+ return () => {
+ document.removeEventListener('mousedown', handler)
+ }
+ }, [isExpanded])
+
+ const [banUser, { isLoading: isLoadingBan }] = useBanUserMutation()
+
+ return (
+ <>
+ {imagesUrl.length ? (
+
+
+ {imagesUrl?.map((image, index) => (
+
+
+
+ ))}
+
+
+
+ ) : (
+
+ {t.user_info.not_found}
+
+ )}
+ >
+ )
+}
+
+export default memo(Post)
diff --git a/src/widgets/superAdmin/superAdmin.tsx b/src/widgets/superAdmin/superAdmin.tsx
new file mode 100644
index 00000000..c4750401
--- /dev/null
+++ b/src/widgets/superAdmin/superAdmin.tsx
@@ -0,0 +1,5 @@
+import { FC } from 'react'
+
+export const Admin: FC = () => {
+ return Admin Page
+}
diff --git a/src/widgets/superAdmin/userList/DebouncedInput.tsx b/src/widgets/superAdmin/userList/DebouncedInput.tsx
new file mode 100644
index 00000000..3017496f
--- /dev/null
+++ b/src/widgets/superAdmin/userList/DebouncedInput.tsx
@@ -0,0 +1,38 @@
+import React, { ComponentPropsWithoutRef, useEffect, useState } from 'react'
+
+import { Input } from '@/shared/components'
+
+type Props = {
+ callback: (value: string) => void
+} & Omit, 'onChange'>
+
+export const DebouncedInput = ({ callback, ...rest }: Props) => {
+ const [debouncedValue, setDebouncedValue] = useState(null)
+ const [valueInput, setValueInput] = useState('')
+
+ useEffect(() => {
+ const debounceTimeout = setTimeout(() => {
+ if (debouncedValue !== null) {
+ callback(debouncedValue)
+ setDebouncedValue(null)
+ }
+ }, 500)
+
+ return () => clearTimeout(debounceTimeout)
+ }, [debouncedValue, callback])
+
+ const handleInputChange = (e: string) => {
+ setValueInput(e)
+ setDebouncedValue(e)
+ }
+
+ return (
+
+ )
+}
diff --git a/src/widgets/superAdmin/userList/ModalAction.tsx b/src/widgets/superAdmin/userList/ModalAction.tsx
new file mode 100644
index 00000000..b19f6382
--- /dev/null
+++ b/src/widgets/superAdmin/userList/ModalAction.tsx
@@ -0,0 +1,81 @@
+import { ReactNode, useEffect, useState } from 'react'
+
+import Link from 'next/link'
+
+import { BlockIcon } from '@/shared/assets/icons/BlockIcon'
+import { DeleteUserIcon } from '@/shared/assets/icons/DeleteUserIcon'
+import { EllipsisIcon } from '@/shared/assets/icons/EllipsisIcon'
+import { CustomDropdown, CustomDropdownItemWithIcon } from '@/shared/components'
+import { useTranslation } from '@/shared/lib'
+
+type Props = {
+ trigger: ReactNode
+ userId: number
+ userName: string
+ addValuesUser: (id: number, name: string) => void
+ addValuesBanUser: (id: number, name: string) => void
+ valueBanUser: any
+ banUsers: any
+}
+
+export const ModalAction = ({
+ trigger,
+ userId,
+ userName,
+ addValuesUser,
+ valueBanUser,
+ addValuesBanUser,
+ banUsers,
+}: Props) => {
+ const { t } = useTranslation()
+ const [isBan, setIsBan] = useState(banUsers)
+
+ const addValuesForDeleteUser = () => {
+ addValuesUser(userId, userName)
+ }
+ const addValuesForUnBanUser = () => {
+ addValuesBanUser(userId, userName)
+ }
+ const addValuesForBanUser = () => {
+ valueBanUser(userId, userName)
+ }
+
+ return (
+
+ }
+ title={t.user_list.delete_user}
+ onClick={addValuesForDeleteUser}
+ />
+ {banUsers ? (
+ }
+ title={t.user_list.unBan}
+ onClick={addValuesForUnBanUser}
+ />
+ ) : (
+ }
+ onClick={addValuesForBanUser}
+ title={t.user_list.ban}
+ />
+ )}
+
+
+ }
+ title={t.user_list.more}
+ />
+
+
+ )
+}
diff --git a/src/widgets/superAdmin/userList/UserList.module.scss b/src/widgets/superAdmin/userList/UserList.module.scss
new file mode 100644
index 00000000..233c4b56
--- /dev/null
+++ b/src/widgets/superAdmin/userList/UserList.module.scss
@@ -0,0 +1,44 @@
+.search {
+ width: 750px;
+}
+
+.select {
+ width: 190px;
+ margin-bottom: 4px;
+}
+
+.panelSearchAndSort {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.table {
+ border: 1px solid var(--color-dark-500);
+
+ th {
+ width: 250px;
+ background-color: var(--color-dark-500);
+ }
+
+ th,
+ td {
+ padding: 20px 30px;
+ text-align: left;
+ border-bottom: 1px solid var(--color-dark-500);
+ }
+
+ .date {
+ cursor: pointer;
+
+ display: flex;
+ gap: 4px;
+ align-items: center;
+
+ width: 450px;
+ }
+}
+
+.ellipsis {
+ cursor: pointer;
+}
diff --git a/src/widgets/superAdmin/userList/UserList.tsx b/src/widgets/superAdmin/userList/UserList.tsx
new file mode 100644
index 00000000..1dcce6c6
--- /dev/null
+++ b/src/widgets/superAdmin/userList/UserList.tsx
@@ -0,0 +1,278 @@
+import React, { useEffect, useState } from 'react'
+
+import { useRouter } from 'next/router'
+
+import s from './UserList.module.scss'
+
+import {
+ useUnBanUserMutation,
+ useBanUserMutation,
+ useDeleteUserMutation,
+ useGetUsersMutation,
+} from '@/entities/users/api/usersApi'
+import { BlockIcon } from '@/shared/assets/icons/BlockIcon'
+import { EllipsisIcon } from '@/shared/assets/icons/EllipsisIcon'
+import { OptionsType, Pagination, SelectCustom } from '@/shared/components'
+import { UserBlockStatus, SortDirection } from '@/shared/constants/enum'
+import { useFetchLoader, useTranslation } from '@/shared/lib'
+import usePagination from '@/shared/lib/hooks/usePagination'
+import { useSortBy } from '@/shared/lib/hooks/useSortBy'
+import { ModalBan } from '@/widgets/superAdmin/userList/banUser/ModalBan'
+import { DebouncedInput } from '@/widgets/superAdmin/userList/DebouncedInput'
+import { ModalDelete } from '@/widgets/superAdmin/userList/deleteUser/ModalDelete'
+import { getValueByLang, statusType } from '@/widgets/superAdmin/userList/getValueByLang'
+import { ModalAction } from '@/widgets/superAdmin/userList/ModalAction'
+import { ModalUnBan } from '@/widgets/superAdmin/userList/unBanUser/ModalUnBan'
+
+export type ShowModalType = {
+ isShow: boolean
+ userId: number | null
+ userName: string | null
+}
+export type ShowModalBanType = {
+ isShow: boolean
+ userId: number | null
+ userName: string | null
+}
+
+export const UserList = () => {
+ const { t } = useTranslation()
+ const router = useRouter()
+
+ const [users, setUsers] = useState([])
+ const [valuePagination, setValuePagination] = useState(null)
+ const [valueSearch, setValueSearch] = useState('')
+ const [defaultValue, setDefaultValue] = useState(() => {
+ if (typeof window !== 'undefined') {
+ return (
+ (localStorage.getItem('lang') as statusType) ?? (t.user_list.not_selected as statusType)
+ )
+ }
+
+ return t.user_list.not_selected as statusType
+ })
+ const [showModalUnban, setShowModalUnban] = useState({
+ isShow: false,
+ userId: null,
+ userName: null,
+ })
+ const [showModalDelete, setShowModalDelete] = useState({
+ isShow: false,
+ userId: null,
+ userName: null,
+ })
+ const [showModalBan, setShowModalBan] = useState({
+ isShow: false,
+ userId: null,
+ userName: null,
+ })
+ const [banUser, { isLoading: isLoadingBan }] = useBanUserMutation()
+ const [deleteUser, { isLoading, isSuccess }] = useDeleteUserMutation()
+ const [data] = useGetUsersMutation()
+ const [unblockUser, { isLoading: isLoadingUnBan, isSuccess: isSuccessUnBan }] =
+ useUnBanUserMutation()
+ const { icon, onSortChange, sort } = useSortBy()
+ const { currentPage, setCurrentPage, pageSize, setPageSize, sortBy, setSortBy } = usePagination()
+
+ const options: OptionsType[] = [
+ { label: t.user_list.not_selected, value: t.user_list.not_selected },
+ { label: t.user_list.blocked, value: t.user_list.blocked },
+ { label: t.user_list.not_blocked, value: t.user_list.not_blocked },
+ ]
+
+ const status = {
+ [t.user_list.not_selected]: UserBlockStatus.ALL,
+ [t.user_list.blocked]: UserBlockStatus.BLOCKED,
+ [t.user_list.not_blocked]: UserBlockStatus.UNBLOCKED,
+ }
+
+ const onSelectChange = (value: statusType) => {
+ const currentValue = getValueByLang(value)
+
+ localStorage.setItem('lang', t.user_list[currentValue as keyof typeof t.user_list])
+ setDefaultValue(t.user_list[currentValue as keyof typeof t.user_list] as statusType)
+ }
+
+ useEffect(() => {
+ onSelectChange(defaultValue)
+ }, [router.locale])
+ const valueBanUser = (id: number, name: string) => {
+ setShowModalBan({
+ userId: id,
+ userName: name,
+ isShow: true,
+ })
+ }
+
+ useEffect(() => {
+ const initObjectUsers: GetUsersType = {
+ pageSize,
+ pageNumber: currentPage as number,
+ sortBy,
+ sortDirection: sort === 'default' ? SortDirection.DESC : (sort as SortDirection),
+ searchTerm: valueSearch,
+ statusFilter: status[defaultValue as string] as UserBlockStatus,
+ }
+
+ data(initObjectUsers)
+ .unwrap()
+ .then(res => {
+ setUsers(res.data.getUsers.users)
+ setValuePagination(res.data.getUsers.pagination)
+ })
+ .catch(er => console.error(er))
+ }, [
+ data,
+ currentPage,
+ isSuccess,
+ valueSearch,
+ pageSize,
+ defaultValue,
+ sort,
+ isSuccessUnBan,
+ isLoadingBan,
+ sortBy,
+ ])
+
+ const onDebounce = (value: string) => {
+ setValueSearch(value)
+ }
+
+ const onPageSizeChange = (value: number) => {
+ setPageSize(value)
+ }
+
+ const onCurrentPageChange = (value: number | string) => {
+ setCurrentPage(value)
+ }
+
+ const addValuesUser = (id: number, name: string) => {
+ setShowModalDelete({
+ userId: id,
+ userName: name,
+ isShow: true,
+ })
+ }
+ const addValuesUnBanUser = (id: number, name: string) => {
+ setShowModalUnban({
+ userId: id,
+ userName: name,
+ isShow: true,
+ })
+ }
+
+ const onDeleteUser = () => {
+ const id = showModalDelete.userId
+
+ if (id) {
+ deleteUser({ userId: id })
+ }
+ !isLoading &&
+ setShowModalDelete({
+ userId: null,
+ userName: null,
+ isShow: false,
+ })
+ }
+
+ const onChangeSortBy = (e: React.MouseEvent, key: string) => {
+ setSortBy(`${e.currentTarget.innerText}`)
+ onSortChange(key)
+ }
+
+ useFetchLoader(isLoading || isLoadingBan)
+
+ return (
+
+
+
+
+
+ {t.user_list.id}
+ onChangeSortBy(e, 'name')}
+ className="flex items-center gap-1 cursor-pointer"
+ >
+ {t.user_list.name}
+ {icon('name')}
+
+ {t.user_list.profile}
+ onChangeSortBy(e, 'date')} className={s.date}>
+ {t.user_list.date}
+ {icon('date')}
+
+
+ {users.map((user: User) => (
+ <>
+
+
+ {user.userBan && } {user.id}
+
+ {user.userName}
+ {user.profile.userName}
+
+ {new Date(user.profile.createdAt).toLocaleDateString('ru-RU')}
+ }
+ userId={user.id}
+ banUsers={user.userBan}
+ userName={user.userName}
+ addValuesUser={addValuesUser}
+ addValuesBanUser={addValuesUnBanUser}
+ valueBanUser={valueBanUser}
+ />
+
+
+ >
+ ))}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/widgets/superAdmin/userList/banUser/ModalBan.tsx b/src/widgets/superAdmin/userList/banUser/ModalBan.tsx
new file mode 100644
index 00000000..b7c8e05f
--- /dev/null
+++ b/src/widgets/superAdmin/userList/banUser/ModalBan.tsx
@@ -0,0 +1,123 @@
+import { Dispatch, useEffect, useState } from 'react'
+
+import { useRouter } from 'next/router'
+
+import { useUnBanUserMutation } from '@/entities/users/api/usersApi'
+import { InputField } from '@/shared'
+import { Button, OptionsType, SelectCustom, Typography } from '@/shared/components'
+import { Modal } from '@/shared/components/modals'
+import { useTranslation } from '@/shared/lib'
+import { BanType, getValueBanByLang } from '@/widgets/superAdmin/userList/getValueByLang'
+import { ShowModalBanType } from '@/widgets/superAdmin/userList/UserList'
+
+type Props = {
+ isOpen: boolean
+ userName: string | null
+ setShowModalBan: Dispatch
+ showModalBan: any
+ banUser: any
+ isLoadingBan: boolean
+}
+
+export const ModalBan = ({
+ isOpen,
+ userName,
+ setShowModalBan,
+ showModalBan,
+ banUser,
+ isLoadingBan,
+}: Props) => {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const [unblockUser, { isLoading: isLoadingUnBan, isSuccess: isSuccessUnBan }] =
+ useUnBanUserMutation()
+ const [selectedOption, setSelectedOption] = useState('' as BanType)
+
+ const [inputValue, setInputValue] = useState('')
+
+ const handleSelectChange = (value: BanType) => {
+ const currentValue = getValueBanByLang(value)
+
+ localStorage.setItem('lang', t.user_list[currentValue as keyof typeof t.user_list])
+ setSelectedOption(t.user_list[currentValue as keyof typeof t.user_list] as BanType)
+ }
+
+ useEffect(() => {
+ handleSelectChange(selectedOption)
+ }, [router.locale])
+
+ const onBanUser = () => {
+ const id = showModalBan.userId
+
+ if (id) {
+ if (selectedOption === t.user_list.another_reason) {
+ banUser({ banReason: inputValue, userId: id })
+ } else {
+ banUser({ banReason: selectedOption, userId: id })
+ }
+ } else {
+ unblockUser({
+ userId: id,
+ })
+ }
+ !isLoadingBan &&
+ setShowModalBan({
+ userId: null,
+ userName: null,
+ isShow: false,
+ })
+ setSelectedOption('' as BanType)
+ setInputValue('')
+ }
+
+ const onCloseModal = () => {
+ setShowModalBan({
+ userId: null,
+ userName: null,
+ isShow: false,
+ })
+ setInputValue('')
+ setSelectedOption('' as BanType)
+ }
+
+ const options: OptionsType[] = [
+ // { label: t.user_list.reason_for_ban, value: t.user_list.reason_for_ban },
+ { label: t.user_list.bad_behavior, value: t.user_list.bad_behavior },
+ { label: t.user_list.advertising_placement, value: t.user_list.advertising_placement },
+ { label: t.user_list.another_reason, value: t.user_list.another_reason },
+ ]
+
+ return (
+
+
+ {t.user_list.are_you_sure_you}
+
+ {` ${userName}?`}
+ {selectedOption === t.user_list.another_reason ? (
+ setInputValue(e.target.value)}
+ placeholder={t.user_list.another_reason}
+ label={''}
+ />
+ ) : (
+
+ )}
+
+
+
+ {t.user_list.no}
+
+
+ {t.user_list.yes}
+
+
+
+ )
+}
diff --git a/src/widgets/superAdmin/userList/banUser/modalBan.module.scss b/src/widgets/superAdmin/userList/banUser/modalBan.module.scss
new file mode 100644
index 00000000..ba917318
--- /dev/null
+++ b/src/widgets/superAdmin/userList/banUser/modalBan.module.scss
@@ -0,0 +1,8 @@
+.select {
+ z-index: 100;
+}
+
+.sss {
+ z-index: 101;
+}
+
diff --git a/src/widgets/superAdmin/userList/deleteUser/ModalDelete.tsx b/src/widgets/superAdmin/userList/deleteUser/ModalDelete.tsx
new file mode 100644
index 00000000..befbc007
--- /dev/null
+++ b/src/widgets/superAdmin/userList/deleteUser/ModalDelete.tsx
@@ -0,0 +1,41 @@
+import { Dispatch } from 'react'
+
+import { Button, Typography } from '@/shared/components'
+import { Modal } from '@/shared/components/modals'
+import { useTranslation } from '@/shared/lib'
+import { ShowModalType } from '@/widgets/superAdmin/userList/UserList'
+
+type Props = {
+ isOpen: boolean
+ userName: string | null
+ setShowModalDelete: Dispatch
+ onDeleteUser: () => void
+}
+export const ModalDelete = ({ isOpen, userName, setShowModalDelete, onDeleteUser }: Props) => {
+ const { t } = useTranslation()
+
+ const onCloseModal = () => {
+ setShowModalDelete({
+ userId: null,
+ userName: null,
+ isShow: false,
+ })
+ }
+
+ return (
+
+
+ {t.user_list.confirmation}
+
+ {` ${userName}?`}
+
+
+ {t.user_list.no}
+
+
+ {t.user_list.yes}
+
+
+
+ )
+}
diff --git a/src/widgets/superAdmin/userList/getValueByLang.ts b/src/widgets/superAdmin/userList/getValueByLang.ts
new file mode 100644
index 00000000..27bf1f88
--- /dev/null
+++ b/src/widgets/superAdmin/userList/getValueByLang.ts
@@ -0,0 +1,36 @@
+export type statusType =
+ | 'Not Selected'
+ | 'Blocked'
+ | 'Not Blocked'
+ | 'Не выбрано'
+ | 'Заблокировано'
+ | 'Не заблокировано'
+
+export type BanType = 'Bad behavior' | 'Advertising placement' | 'Another reason'
+
+export const getValueByLang = (value: statusType) => {
+ const values = {
+ 'Not Selected': 'not_selected',
+ Blocked: 'blocked',
+ 'Not Blocked': 'not_blocked',
+ 'Не выбрано': 'not_selected',
+ Заблокировано: 'blocked',
+ 'Не заблокировано': 'not_blocked',
+ }
+
+ return values[value]
+}
+export const getValueBanByLang = (value: BanType) => {
+ const values = {
+ // 'Reason for ban': 'reason_for_ban',
+ 'Bad behavior': 'bad_behavior',
+ 'Advertising placement': 'advertising_placement',
+ 'Another reason': 'another_reason',
+ 'Причина блокировки': 'reason_for_ban',
+ 'Плохое поведение': 'bad_behavior',
+ 'Размещение рекламы': 'advertising_placement',
+ 'Другая причина': 'another_reason',
+ }
+
+ return values[value]
+}
diff --git a/src/widgets/superAdmin/userList/moreInformation/MoreInformation.tsx b/src/widgets/superAdmin/userList/moreInformation/MoreInformation.tsx
new file mode 100644
index 00000000..0bc414bf
--- /dev/null
+++ b/src/widgets/superAdmin/userList/moreInformation/MoreInformation.tsx
@@ -0,0 +1,58 @@
+import { FC, useEffect, useState } from 'react'
+
+import { useRouter } from 'next/router'
+
+import { useGetUserMutation } from '@/entities/users/api/usersApi'
+import { ArrowBack } from '@/shared/assets/icons/ArrowBack'
+import { Typography } from '@/shared/components'
+import { useFetchLoader, useTranslation } from '@/shared/lib'
+import { UserOverview } from '@/widgets/superAdmin/userList/moreInformation/table-info/UserOverview'
+import { UserInfo } from '@/widgets/superAdmin/userList/moreInformation/userInfo/UserInfo'
+
+const MoreInformation: FC = () => {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const { userId } = router.query
+
+ const [user, setUser] = useState(null)
+
+ const [getUser, { isLoading }] = useGetUserMutation()
+
+ useEffect(() => {
+ if (userId) {
+ getUser(userId)
+ .unwrap()
+ .then(res => {
+ setUser(res.data.getUser)
+ })
+ }
+ }, [userId])
+
+ useFetchLoader(isLoading)
+
+ return (
+
+
router.push('/userList')}>
+
+
{t.user_list.backToUserList}
+
+ {user && (
+ <>
+
+
+ >
+ )}
+
+ )
+}
+
+export default MoreInformation
diff --git a/src/widgets/superAdmin/userList/moreInformation/table-info/UserOverview.module.scss b/src/widgets/superAdmin/userList/moreInformation/table-info/UserOverview.module.scss
new file mode 100644
index 00000000..aca1f1ea
--- /dev/null
+++ b/src/widgets/superAdmin/userList/moreInformation/table-info/UserOverview.module.scss
@@ -0,0 +1,17 @@
+.main {
+ table-layout: fixed;
+ width: 100%;
+ margin: 50px 0 30px;
+
+ th {
+ cursor: pointer;
+ width: 25%;
+ text-align: center;
+ border-bottom: 2px solid var(--color-dark-100);
+ }
+
+ .active {
+ color: var(--color-accent-500);
+ border-bottom: 2px solid var(--color-accent-500);
+ }
+}
diff --git a/src/widgets/superAdmin/userList/moreInformation/table-info/UserOverview.tsx b/src/widgets/superAdmin/userList/moreInformation/table-info/UserOverview.tsx
new file mode 100644
index 00000000..c6efcb00
--- /dev/null
+++ b/src/widgets/superAdmin/userList/moreInformation/table-info/UserOverview.tsx
@@ -0,0 +1,81 @@
+import React, { useEffect, useState } from 'react'
+
+import { Payments } from './payments/Payments'
+import { UploadedPhotos } from './photos/UploadedPhotos'
+import s from './UserOverview.module.scss'
+
+import { useGetPostsByUserMutation } from '@/entities/users/api/usersApi'
+import { Typography } from '@/shared/components'
+import { useTranslation } from '@/shared/lib'
+import { Follow } from '@/widgets/superAdmin/userList/moreInformation/table-info/follow-info/Follow'
+
+type Props = {
+ userId: number
+}
+export type ActiveStyle = 'photos' | 'payments' | 'followers' | 'following'
+
+export const UserOverview = ({ userId }: Props) => {
+ const { t } = useTranslation()
+
+ const [posts, setPosts] = useState(null)
+ const [activeStyle, setActiveStyle] = useState('photos')
+
+ const [getPosts] = useGetPostsByUserMutation()
+
+ useEffect(() => {
+ getPosts({ id: userId, endCursorId: 0 })
+ .unwrap()
+ .then(res => {
+ setPosts(res.data.getPostsByUser)
+ })
+ }, [setPosts, userId])
+
+ const handleTableClick = (e: React.MouseEvent) => {
+ const target = e.target as HTMLElement
+
+ setActiveStyle(target.title as ActiveStyle)
+ }
+
+ const getDetails = (name: ActiveStyle) => {
+ const component = {
+ photos: ,
+ payments: ,
+ followers: ,
+ following: ,
+ }
+
+ return component[name]
+ }
+
+ return (
+
+
+
+
+
+
+ {t.user_info.uploaded_photos}
+
+
+
+
+ {t.user_info.payments}
+
+
+
+
+ {t.user_info.followers}
+
+
+
+
+ {t.user_info.following}
+
+
+
+
+
+ {getDetails(activeStyle)}
+
+ )
+}
diff --git a/src/widgets/superAdmin/userList/moreInformation/table-info/follow-info/Follow.module.scss b/src/widgets/superAdmin/userList/moreInformation/table-info/follow-info/Follow.module.scss
new file mode 100644
index 00000000..bc7e189f
--- /dev/null
+++ b/src/widgets/superAdmin/userList/moreInformation/table-info/follow-info/Follow.module.scss
@@ -0,0 +1,5 @@
+@use 'src/shared/assets/mixins';
+
+.table {
+ @include mixins.table_styles;
+}
diff --git a/src/widgets/superAdmin/userList/moreInformation/table-info/follow-info/Follow.tsx b/src/widgets/superAdmin/userList/moreInformation/table-info/follow-info/Follow.tsx
new file mode 100644
index 00000000..b0d06eea
--- /dev/null
+++ b/src/widgets/superAdmin/userList/moreInformation/table-info/follow-info/Follow.tsx
@@ -0,0 +1,121 @@
+import React, { useEffect, useState } from 'react'
+
+import Link from 'next/link'
+
+import styles from './Follow.module.scss'
+
+import { useGetFollowersMutation, useGetFollowingMutation } from '@/entities/users/api/usersApi'
+import { Pagination, Typography } from '@/shared/components'
+import { SortDirection } from '@/shared/constants/enum'
+import { useFetchLoader, useTranslation } from '@/shared/lib'
+import { useSortBy } from '@/shared/lib/hooks/useSortBy'
+import { getPageItems } from '@/widgets/profileSettings/my-payments/utils/getPageItems'
+
+type Props = {
+ tab: 'followers' | 'following'
+ userId: number
+}
+export const Follow = ({ tab, userId }: Props) => {
+ const { t } = useTranslation()
+
+ const [pageNumber, setPageNumber] = useState(1)
+ const [pageSize, setPageSize] = useState(10)
+ const [followContent, setFollowContent] = useState(null)
+ const [items, setItems] = useState([])
+
+ const [getFollowers, { isLoading: loadFollowers }] = useGetFollowersMutation()
+ const [getFollowing, { isLoading: loadFollowing }] = useGetFollowingMutation()
+
+ const { icon, sort, onSortChange } = useSortBy()
+
+ useFetchLoader(loadFollowers)
+ useFetchLoader(loadFollowing)
+
+ useEffect(() => {
+ setFollowContent(null)
+ }, [tab])
+
+ useEffect(() => {
+ const initialObj = {
+ pageSize,
+ pageNumber,
+ sortBy: 'createdAt',
+ sortDirection: sort === 'default' ? SortDirection.DESC : sort,
+ userId,
+ }
+
+ const func = tab === 'followers' ? getFollowers : getFollowing
+
+ func(initialObj)
+ .unwrap()
+ .then(res => {
+ !followContent && setFollowContent(res.data.getFollowers)
+ })
+
+ const followItems = followContent?.items
+
+ setItems(getPageItems(pageNumber as number, pageSize, followItems || []))
+ }, [pageSize, pageNumber, followContent, sort])
+
+ return (
+
+ {items.length ? (
+ <>
+
+
+
+ {t.user_info.usertId}
+ onSortChange('name')}
+ className="flex items-center gap-1 cursor-pointer"
+ >
+ {t.user_info.userName}
+ {icon('name')}
+
+ {t.user_info.profileLink}
+ onSortChange('subscription')}
+ className="flex items-center gap-1 cursor-pointer"
+ >
+ {t.user_info.subscriptionDate}
+ {icon('subscription')}
+
+
+ {items.map((item: FollowItems) => (
+
+ {item.userId}
+ {item.userName}
+
+
+
+ {item.userName}
+
+
+
+ {new Date(item.createdAt).toLocaleDateString('ru-RU')}
+
+ ))}
+
+
+
+ >
+ ) : (
+
+ {t.user_info.not_found}
+
+ )}
+
+ )
+}
diff --git a/src/widgets/superAdmin/userList/moreInformation/table-info/payments/Payments.module.scss b/src/widgets/superAdmin/userList/moreInformation/table-info/payments/Payments.module.scss
new file mode 100644
index 00000000..5169856a
--- /dev/null
+++ b/src/widgets/superAdmin/userList/moreInformation/table-info/payments/Payments.module.scss
@@ -0,0 +1,6 @@
+@use 'src/shared/assets/mixins';
+
+.table {
+ @include mixins.table_styles;
+}
+
diff --git a/src/widgets/superAdmin/userList/moreInformation/table-info/payments/Payments.tsx b/src/widgets/superAdmin/userList/moreInformation/table-info/payments/Payments.tsx
new file mode 100644
index 00000000..efb09ac1
--- /dev/null
+++ b/src/widgets/superAdmin/userList/moreInformation/table-info/payments/Payments.tsx
@@ -0,0 +1,113 @@
+import { useEffect, useState } from 'react'
+
+import styles from './Payments.module.scss'
+
+import { useGetPaymentsByUserMutation } from '@/entities/users/api/usersApi'
+import { Pagination, Typography } from '@/shared/components'
+import { useFetchLoader, useTranslation } from '@/shared/lib'
+import { getPageItems } from '@/widgets/profileSettings/my-payments/utils/getPageItems'
+
+type Props = {
+ userId: number
+}
+
+export const Payments = ({ userId }: Props) => {
+ const { t } = useTranslation()
+
+ const [currentPage, setCurrentPage] = useState(1)
+ const [pageSize, setPageSize] = useState(10)
+ const [paymentUser, setPaymentUser] = useState(null)
+ const [array, setArray] = useState([])
+
+ const [data, { isLoading }] = useGetPaymentsByUserMutation()
+
+ const tabType: { [key: string]: string } = {
+ DAY: '1 day',
+ WEEKLY: '7 days',
+ MONTHLY: '1 month',
+ STRIPE: 'Stripe',
+ PAYPAL: 'PayPal',
+ }
+
+ const onCurrentPageChange = (value: number | string) => {
+ setCurrentPage(value)
+ }
+
+ const onPageSizeChange = (value: number) => {
+ setPageSize(value)
+ }
+
+ useEffect(() => {
+ const initialObject = {
+ userId: userId,
+ pageSize,
+ pageNumber: currentPage as number,
+ sortBy: 'createdAt',
+ sortDirection: 'desc',
+ }
+
+ if (!paymentUser) {
+ data(initialObject)
+ .unwrap()
+ .then(res => {
+ setPaymentUser(res.data.getPaymentsByUser)
+ })
+ }
+
+ const paymentArray = paymentUser?.items
+
+ setArray(
+ getPageItems(currentPage as number, pageSize, paymentArray || [])
+ )
+ }, [userId, currentPage, pageSize, setPaymentUser, setArray, paymentUser])
+
+ useFetchLoader(isLoading)
+
+ return (
+ !isLoading && (
+
+ {array.length ? (
+ <>
+
+
+
+ {t.date_of_payment}
+ {t.end_date_of_subscription}
+ {t.amount}, $
+ {t.subscription_type}
+ {t.payment_type}
+
+ {array.map((item: SuperAdminItemsByUser) => (
+
+ {new Date(item.dateOfPayment).toLocaleDateString('ru-RU')}
+ {new Date(item.endDate).toLocaleDateString('ru-RU')}
+ ${item.price}
+ {tabType[item.type]}
+ {tabType[item.paymentType]}
+
+ ))}
+
+
+
+ >
+ ) : (
+
+ {t.user_info.not_found}
+
+ )}
+
+ )
+ )
+}
diff --git a/src/widgets/superAdmin/userList/moreInformation/table-info/photos/UploadedPhotos.module.scss b/src/widgets/superAdmin/userList/moreInformation/table-info/photos/UploadedPhotos.module.scss
new file mode 100644
index 00000000..86ed2e62
--- /dev/null
+++ b/src/widgets/superAdmin/userList/moreInformation/table-info/photos/UploadedPhotos.module.scss
@@ -0,0 +1,5 @@
+@use 'src/shared/assets/mixins';
+
+.table {
+ @include mixins.table_photo
+}
diff --git a/src/widgets/superAdmin/userList/moreInformation/table-info/photos/UploadedPhotos.tsx b/src/widgets/superAdmin/userList/moreInformation/table-info/photos/UploadedPhotos.tsx
new file mode 100644
index 00000000..64957b8e
--- /dev/null
+++ b/src/widgets/superAdmin/userList/moreInformation/table-info/photos/UploadedPhotos.tsx
@@ -0,0 +1,42 @@
+import Image from 'next/image'
+
+import s from './UploadedPhotos.module.scss'
+
+import { Typography } from '@/shared/components'
+import { useTranslation } from '@/shared/lib'
+
+type Props = {
+ photos: ImagePost[]
+}
+export const UploadedPhotos = ({ photos }: Props) => {
+ const { t } = useTranslation()
+ const rows = []
+
+ for (let i = 0; i < photos.length; i += 4) {
+ rows.push(photos.slice(i, i + 4))
+ }
+
+ return (
+ <>
+ {rows.length ? (
+
+
+ {rows.map((row, rowIndex) => (
+
+ {row.map(photo => (
+
+
+
+ ))}
+
+ ))}
+
+
+ ) : (
+
+ {t.user_info.not_found}
+
+ )}
+ >
+ )
+}
diff --git a/src/widgets/superAdmin/userList/moreInformation/userInfo/UserInfo.tsx b/src/widgets/superAdmin/userList/moreInformation/userInfo/UserInfo.tsx
new file mode 100644
index 00000000..5dd9f181
--- /dev/null
+++ b/src/widgets/superAdmin/userList/moreInformation/userInfo/UserInfo.tsx
@@ -0,0 +1,47 @@
+import Link from 'next/link'
+
+import { Typography } from '@/shared/components'
+import { AvatarSmallView } from '@/shared/components/avatarSmallView'
+import { useTranslation } from '@/shared/lib'
+
+type Props = {
+ avatar: string
+ name: { first: string; last: string }
+ userName: string
+ userId: number
+ date: Date
+}
+
+export const UserInfo = ({ avatar, name, userName, userId, date }: Props) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+
+ {name.first} {name.last}
+
+
+ {userName}
+
+
+
+
+
+ {t.user_info.usertId}
+
+ {userId}
+
+
+
+ {t.user_info.profileDate}
+
+ {new Date(date).toLocaleDateString('ru-RU')}
+
+
+
+
+ )
+}
diff --git a/src/widgets/superAdmin/userList/unBanUser/ModalUnBan.tsx b/src/widgets/superAdmin/userList/unBanUser/ModalUnBan.tsx
new file mode 100644
index 00000000..3727065a
--- /dev/null
+++ b/src/widgets/superAdmin/userList/unBanUser/ModalUnBan.tsx
@@ -0,0 +1,65 @@
+import { Dispatch, useState } from 'react'
+
+import { useUnBanUserMutation } from '@/entities/users/api/usersApi'
+import { Button, Typography } from '@/shared/components'
+import { Modal } from '@/shared/components/modals'
+import { useTranslation } from '@/shared/lib'
+import { ShowModalType } from '@/widgets/superAdmin/userList/UserList'
+
+type Props = {
+ setShowModalUnban: Dispatch
+ showModalUnban: any
+ unblockUser: any
+ isLoadingUnBan: boolean
+}
+export const ModalUnBan = ({
+ setShowModalUnban,
+ showModalUnban,
+ unblockUser,
+ isLoadingUnBan,
+}: Props) => {
+ const { t } = useTranslation()
+
+ const onUnbanUser = () => {
+ const id = showModalUnban.userId
+
+ if (id) {
+ unblockUser({ userId: id })
+ }
+ !isLoadingUnBan &&
+ setShowModalUnban({
+ userId: null,
+ userName: null,
+ isShow: false,
+ })
+ }
+ const onCloseModal = () => {
+ setShowModalUnban({
+ userId: null,
+ userName: null,
+ isShow: false,
+ })
+ }
+
+ return (
+
+
+ {t.user_list.confirmation_unBan}
+
+ {` ${showModalUnban.userName}?`}
+
+
+ {t.user_list.no}
+
+
+ {t.user_list.yes}
+
+
+
+ )
+}
diff --git a/types/devices.d.ts b/types/devices.d.ts
new file mode 100644
index 00000000..0dbf7b9e
--- /dev/null
+++ b/types/devices.d.ts
@@ -0,0 +1,11 @@
+type Device = {
+ deviceId: number,
+ ip: string,
+ lastActive: Date,
+ browserName: string,
+ browserVersion: string,
+ deviceName: string,
+ osName: string,
+ osVersion: string,
+ deviceType: string,
+}
\ No newline at end of file
diff --git a/types/notification.d.ts b/types/notification.d.ts
new file mode 100644
index 00000000..9cc67f9f
--- /dev/null
+++ b/types/notification.d.ts
@@ -0,0 +1,15 @@
+type NotificationItems = {
+ id: number
+ isRead: boolean
+ message: string
+ notifyAt: Date
+}
+interface INotification {
+ pageSize: number
+ totalCount: number
+ items: NotificationItems[]
+}
+
+type MessagesNotif = NotificationItems & {
+ clientId: string;
+}
\ No newline at end of file
diff --git a/types/post.d.ts b/types/post.d.ts
index 3e23b8ee..c0a86b05 100644
--- a/types/post.d.ts
+++ b/types/post.d.ts
@@ -1,23 +1,23 @@
type CountriesDataDict = Record
type CountriesRTKOutput = {
- countriesDataDict: CountriesDataDict
- countriesWithoutCities: City[]
- responseError: boolean
+ countriesDataDict: CountriesDataDict
+ countriesWithoutCities: City[]
+ responseError: boolean
};
type ImagesUrlData = Record
type PublicPostCardProps = {
- postId: number
- ownerId: number
- profileImage?: string | StaticImageData
- description: string
- imagesUrl: ImagesUrlData[]
- userName: string
- firstName: string
- lastName: string
- updatedAt: string
+ postId: number
+ ownerId: number
+ profileImage?: string | StaticImageData
+ description: string
+ imagesUrl: ImagesUrlData[]
+ userName: string
+ firstName: string
+ lastName: string
+ updatedAt: string
}
type PublicPostsResponseData = {
@@ -26,18 +26,48 @@ type PublicPostsResponseData = {
pageSize: number
totalUsers: number
}
+
+type CommentsResponseData = {
+ items: CommentsDataType[]
+ totalCount: number
+ pageSize: number
+}
+
type Owner = {
- firstName: string
- lastName: string
-}
-
- type PostDataType = {
- id: number
- ownerId: number
- userName: string
- description: string
- images: PostImageDTO[]
- owner: Owner
- avatarOwner: string
- updatedAt: string
- }
+ firstName: string
+ lastName: string
+}
+type From = {
+ id: number;
+ username: string;
+ avatars: Avatar[];
+}
+type Avatar = {
+ url: string,
+ width: number,
+ height: number,
+ fileSize: number
+}
+
+type PostDataType = {
+ id: number
+ ownerId: number
+ userName: string
+ description: string
+ images: PostImageDTO[]
+ owner: Owner
+ avatarOwner: string
+ updatedAt: string
+ createdAt:string
+}
+
+type CommentsDataType = {
+ id: number
+ postId: number
+ from:From
+ content:string
+ likeCount:number
+ isLiked:boolean
+ createdAt:string
+
+}
diff --git a/types/subscription.d.ts b/types/subscription.d.ts
new file mode 100644
index 00000000..222ba778
--- /dev/null
+++ b/types/subscription.d.ts
@@ -0,0 +1,14 @@
+type TypeRu = 'Персональный' | 'Бизнес'
+type TypeEn = 'Personal' | 'Business'
+type ValueType = TypeRu | TypeEn
+
+type PriceEn = '$10 per 1 Day' | '$50 per 7 Day' | '$100 per month'
+type PriceRu = '10$ за один день' | '50$ за неделю' | '100$ за месяц'
+type ValuePriceType = PriceEn | PriceRu
+type KeyDataType = ValuePriceType | string
+type LangType = 'en' | 'ru'
+type PriceType = 'price' | 'type'
+
+type DataType = {
+ [key in KeyDataType]: {amount: '10' | '50' | '100', period: 'DAY' | 'WEEKLY' | 'MONTHLY'}
+}
\ No newline at end of file
diff --git a/types/users.d.ts b/types/users.d.ts
new file mode 100644
index 00000000..3bdbb8e1
--- /dev/null
+++ b/types/users.d.ts
@@ -0,0 +1,172 @@
+enum SortDirection {
+ DESC = "desc",
+ ASC = "asc",
+}
+
+enum UserBlockStatus {
+ ALL = "ALL",
+ BLOCKED = "BLOCKED",
+ UNBLOCKED = "UNBLOCKED",
+}
+
+type GetUsersType = {
+ pageSize: number
+ pageNumber: number
+ sortBy: string
+ sortDirection: SortDirection
+ searchTerm: string
+ statusFilter: UserBlockStatus
+}
+
+type Avatar = {
+ url: string
+ width: number
+ height: number
+ fileSize: number
+}
+
+type ProfileUser = {
+ id: number
+ userName: string
+ firstName: string
+ lastName: string
+ city: string
+ dateOfBirth: Date
+ aboutMe: string
+ createdAt: Date
+ avatars: Avatar
+}
+
+type UserBanType = {
+ reason: string
+ createdAt: Date
+}
+
+type User = {
+ id: number
+ userName: string
+ email: string
+ createdAt: Date
+ profile: ProfileUser
+ userBan: UserBanType
+}
+
+type PaginationModel = {
+ pagesCount: number
+ page: number
+ pageSize: number
+ totalCount: number
+}
+
+type UsersResponse = {
+ users: User
+ pagination: PaginationModel
+}
+
+type ImagePost = {
+ id: number
+ createdAt: Date
+ url: string
+ width: number
+ height: number
+ fileSize: number
+}
+
+type Posts = {
+ pagesCount: number
+ pageSize: number
+ totalCount: number
+ items: ImagePost[]
+}
+
+enum SuperAdminStatus {
+ PENDING = PENDING,
+ ACTIVE = ACTIVE,
+ FINISHED = FINISHED,
+ DELETED = DELETED
+}
+
+enum SuperAdminType {
+ MONTHLY = MONTHLY,
+ DAY = DAY,
+ WEEKLY = WEEKLY
+}
+
+enum SuperAdminPaymentType {
+ STRIPE = STRIPE,
+ PAYPAL = PAYPAL,
+ CREDIT_CARD = CREDIT_CARD
+}
+
+enum SuperAdminCurrency {
+ USD = USD,
+ EUR = EUR
+}
+
+type SuperAdminPayments = {
+ id: number,
+ userId: number,
+ paymentMethod: SuperAdminPaymentType,
+ amount: number,
+ currency: SuperAdminCurrency,
+ createdAt: Date,
+ endDate: Date,
+ type: SuperAdminType
+}
+
+type SuperAdminItemsByUser = {
+ id: number
+ businessAccountId: number
+ status: SuperAdminStatus
+ dateOfPayment: Date
+ startDate: Date
+ endDate: Date
+ type: SuperAdminType
+ price: number
+ paymentType: SuperAdminPaymentType
+ payments: SuperAdminPayments[]
+}
+
+type SuperAdminPagePaymentsByUser = {
+ pagesCount: number
+ page: number
+ pageSize: number
+ totalCount: number
+ items: SuperAdminItemsByUser[]
+}
+
+type FollowItems = {
+ id: number
+ userId: number
+ userName: string
+ createdAt: Date
+}
+
+type FollowContent = Omit & {
+ items: FollowItems[]
+}
+
+type PaymentsAllItems = SuperAdminPayments & {
+ userName: string
+ avatars: Avatar[]
+}
+
+type PaymentsAll = Omit & {
+ items: PaymentsAllItems[]
+}
+
+type PostsAllItems = {
+ images: ImagePost[]
+ id: number
+ ownerId: number
+ description: string
+ createdAt: Date
+ updatedAt: Date
+ postOwner: Pick & {
+ avatars: Avatar
+ }
+}
+
+type PostsAll = Omit & {
+ items: PostsAllItems[]
+}
\ No newline at end of file
diff --git a/types/usersFollow.d.ts b/types/usersFollow.d.ts
new file mode 100644
index 00000000..c688eb5d
--- /dev/null
+++ b/types/usersFollow.d.ts
@@ -0,0 +1,35 @@
+type SearchUsersItems = {
+ avatars: Avatar[]
+ createdAt: string
+ firstName: string
+ id: number
+ lastName: string
+ userName: string
+}
+
+type SearchUsers = {
+ items: SearchUsersItems[]
+ nextCursor: number
+ page: number
+ pageSize: number
+ pagesCount: number
+ prevCursor: number
+ totalCount: number
+}
+
+type UserProfile = {
+ aboutMe: string
+ avatars: Avatar[]
+ city: string
+ country: string
+ dateOfBirth: string
+ firstName: string
+ followersCount: number
+ followingCount: number
+ id: number
+ isFollowedBy: boolean
+ isFollowing: boolean
+ lastName: string
+ publicationsCount: number
+ userName: string
+}
\ No newline at end of file