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 ( -

- +
+
+ {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} + + + + + +    +
+ menu-trigger +
+
+ +
+
+
+
+
+
+ +
{ + openModal(id) + } + : () => null + } + > + +
+ +
+ +
+ +
+ + + {userName} + + + {' '} + {description} + +
+ +
+
+ + Owner's avatar + Owner's avatar +
+ + + 2 243 + +   + + "{t.post_view.like}" + + +
+ + {formatDate(updatedAt)} + +
+ +
{ + openModal(id) + } + : () => null + } + > + {t.home.View_all_comments} ({dataAuth && dataAuth.totalCount}) +
+
+
+ setComment(e.target.value)} + placeholder={t.post_view.add_comment} + className={s.InputField} + /> + + +
+
+
+
+ ) +} 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 ( Owner's avatar 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 > {alt} 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 ( +
+
Header
+
+ +
+
+ ) + }, +} 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 ? ( {
- - + {!isDraft ? ( + + ) : ( + + )}
)} = ({ 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' }} > - {filter.name} 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] ? ( - {'postImg'} + {'postImg'} ) : ( )} 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 ( +
+
+ {isAdmin && ( + + InctagramSuperAdmin + + )} + +
+ +
+
+
+ ) +} 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}?`}

+ ) : ( + '' + )} + + {dataAnswer.items.map((el: any) => ( +
+
+ Owner's avatar +
+ + + {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 ( +
+
+ Owner's avatar +
+
+ + + {el.from.username} + + +    + + {el.content} + + {oneComments ? ( + '' + ) : ( + <> +
+ + + +    + + Like: {el.likeCount} + +    + + {t.post_view.answer} + +
+ + )} +
+
+ {oneComments ? ( + '' + ) : ( +
+
{el.isLiked ? : }
+
+ )} +
+
+ ) +} 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'} > - - @@ -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 = ({
-
-
- Owner's avatar -
- - - URLProfiele - - -    - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua - -
- - - -    - - {t.post_view.answer} - -
-
-
-
- -
-
-
-
- Owner's avatar -
- - - 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} - -
-
-
-
- -
-
-
-
- Owner's avatar -
- - - 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) => ( + + ))}
-
+
@@ -277,11 +257,29 @@ export const PostCommentsView = ({ {formatDate(updatedAt)}
+
- - {t.post_view.add_comment} - - + setComment(e.target.value)} + placeholder={t.post_view.add_comment} + className={s.InputField} + /> + {/* setComment(e.target.value)}*/} + {/* placeholder={''}*/} + {/* className={s.updatedAt}*/} + {/*/>*/} + + {/*{t.post_view.add_comment}*/} +
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} + /> +
+
+ + Or + +
+
+ ) +} 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} + + + ) +} 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} + /> +
    + +
    + + )} + + {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 && ( +
    + + + + + + + + + + {array.map((item: IPayments) => ( + + + + + + + + ))} + +
    {t.date_of_payment}{t.end_date_of_subscription}{t.price}{t.subscription_type}{t.payment_type}
    {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 => ( +
    1. + +
      + + {user.userName} + + + {user.firstName ? `${user.firstName} ` : '---- '} + {user.lastName ? user.lastName : '----'} + +
      +
    2. + ))} +
    + ) +} 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?.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 ? ( + <> + + + + + + + + + + {pagedData?.items.map(item => ( + + + + + + + + ))} + +
    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')} +
    + + {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 && ( + + )} + + ) +} + +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 ( +
    +
    + + {userName} +
    + { + 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 ( +
    +
    +
    + +
    +
    + +
    +
    + + + + + + + + + {users.map((user: User) => ( + <> + + + + + + + + ))} + +
    {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')} +
    + {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={''} + /> + ) : ( + + )} + +
    + + +
    +
    + ) +} 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}?`} +
    + + +
    +
    + ) +} 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 ? ( + <> + + + + + + + + + {items.map((item: FollowItems) => ( + + + + + + + ))} + +
    {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')} +
    {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 ? ( + <> + + + + + + + + + + {array.map((item: SuperAdminItemsByUser) => ( + + + + + + + + ))} + +
    {t.date_of_payment}{t.end_date_of_subscription}{t.amount}, ${t.subscription_type}{t.payment_type}
    {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 => ( + + ))} + + ))} + +
    + {'img'} +
    + ) : ( + + {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}?`} +
    + + +
    +
    + ) +} 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