char implementation

This commit is contained in:
dzfelix 2024-10-29 16:53:53 +03:00
parent 4ac2740942
commit 61de5c81e7
16 changed files with 790 additions and 128 deletions

1
.env
View File

@ -4,3 +4,4 @@ NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE

310
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@ant-design/icons": "^5.2.6",
"@ant-design/nextjs-registry": "^1.0.0",
"@contentful/rich-text-react-renderer": "^15.22.9",
"@microsoft/signalr": "^8.0.7",
"agora-rtc-react": "^2.1.0",
"agora-rtc-sdk-ng": "^4.20.2",
"antd": "^5.12.1",
@ -24,6 +25,7 @@
"next-intl": "^3.3.1",
"react": "^18",
"react-dom": "^18",
"react-signalr": "^0.2.24",
"react-slick": "^0.29.0",
"slick-carousel": "^1.8.1",
"styled-components": "^6.1.1"
@ -428,6 +430,19 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@microsoft/signalr": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.7.tgz",
"integrity": "sha512-PHcdMv8v5hJlBkRHAuKG5trGViQEkPYee36LnJQx4xHOQ5LL4X0nEWIxOp5cCtZ7tu+30quz5V3k0b1YNuc6lw==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"eventsource": "^2.0.2",
"fetch-cookie": "^2.0.3",
"node-fetch": "^2.6.7",
"ws": "^7.4.5"
}
},
"node_modules/@next/env": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.3.tgz",
@ -768,6 +783,13 @@
"integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==",
"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==",
"license": "MIT",
"peer": true
},
"node_modules/@swc/helpers": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
@ -974,6 +996,18 @@
"resolved": "https://registry.npmjs.org/@vercel/stega/-/stega-0.1.2.tgz",
"integrity": "sha512-P7mafQXjkrsoyTRppnt0N21udKS9wUmLXHRyP9saLXLHw32j/FgUJ3FscSWgvSqRs4cj7wKZtwqJEvWJ2jbGmA=="
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@ -1821,7 +1855,6 @@
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
@ -1955,6 +1988,52 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/engine.io-client": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
"integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==",
"license": "MIT",
"peer": true,
"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.1.1"
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
@ -2582,6 +2661,24 @@
"node": ">=0.10.0"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/eventsource": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/fast-copy": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-2.1.7.tgz",
@ -2664,6 +2761,16 @@
"node": "^12.20 || >= 14.13"
}
},
"node_modules/fetch-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz",
"integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==",
"license": "Unlicense",
"dependencies": {
"set-cookie-parser": "^2.4.8",
"tough-cookie": "^4.0.0"
}
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -3137,6 +3244,12 @@
"node": ">= 0.4"
}
},
"node_modules/hermes-channel": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/hermes-channel/-/hermes-channel-2.1.2.tgz",
"integrity": "sha512-PVH+X8/S9J6XItQgIRLlsrwXUmb/v13wxvcZgqohnnlUZZOEWbWZ07bLsuQ9dEMnNpT9APvBuVV50W5QmDt4pA==",
"license": "MIT"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -3654,6 +3767,12 @@
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"peer": true
},
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==",
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -3909,8 +4028,7 @@
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nanoid": {
"version": "3.3.7",
@ -4053,6 +4171,26 @@
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@ -4434,11 +4572,16 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"engines": {
"node": ">=6"
}
@ -4457,6 +4600,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -5096,6 +5245,22 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"node_modules/react-signalr": {
"version": "0.2.24",
"resolved": "https://registry.npmjs.org/react-signalr/-/react-signalr-0.2.24.tgz",
"integrity": "sha512-d7Fk2ZibAxUi3t4ovBuGaTSSWzsTLpW8Wh9HCgzPBomkufnykUhe2vS1uBLfbTDlUaaEjoLlQQjk+ZXJoilmhg==",
"license": "MIT",
"dependencies": {
"hermes-channel": "^2.1.2",
"js-cookie": "^2.2.1",
"uuid": "^8.3.2"
},
"peerDependencies": {
"@microsoft/signalr": "^8.0.0",
"react": ">=16.13",
"socket.io-client": "^4.2.0"
}
},
"node_modules/react-slick": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.29.0.tgz",
@ -5168,6 +5333,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@ -5363,6 +5534,12 @@
"node": ">=10"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -5466,6 +5643,36 @@
"jquery": ">=1.8.0"
}
},
"node_modules/socket.io-client": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz",
"integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
@ -5865,6 +6072,27 @@
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@ -6046,6 +6274,15 @@
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
},
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
@ -6085,6 +6322,16 @@
"punycode": "^2.1.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/use-intl": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.17.4.tgz",
@ -6097,6 +6344,15 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@ -6117,6 +6373,12 @@
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/webrtc-adapter": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.0.tgz",
@ -6129,6 +6391,16 @@
"npm": ">=3.10.0"
}
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -6332,6 +6604,36 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"license": "MIT",
"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/xmlhttprequest-ssl": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
"integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
"peer": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -13,6 +13,7 @@
"@ant-design/icons": "^5.2.6",
"@ant-design/nextjs-registry": "^1.0.0",
"@contentful/rich-text-react-renderer": "^15.22.9",
"@microsoft/signalr": "^8.0.7",
"agora-rtc-react": "^2.1.0",
"agora-rtc-sdk-ng": "^4.20.2",
"antd": "^5.12.1",
@ -25,6 +26,7 @@
"next-intl": "^3.3.1",
"react": "^18",
"react-dom": "^18",
"react-signalr": "^0.2.24",
"react-slick": "^0.29.0",
"slick-carousel": "^1.8.1",
"styled-components": "^6.1.1"

View File

@ -0,0 +1,16 @@
import {apiRequest} from "../helpers";
export const getChatList = (locale: string, token: string): Promise<any> => apiRequest({
url: '/chat/chatList',
method: 'get',
locale,
token
});
export const getChatMessages = (locale: string, token: string, group: number): Promise<any> => apiRequest({
url: '/chat/chat_messages/'+group,
method: 'get',
locale,
token
});

View File

@ -1,8 +1,9 @@
'use client'
import React from 'react';
import { unstable_setRequestLocale } from 'next-intl/server';
import { Link } from '../../../../../../navigation';
import { i18nText } from '../../../../../../i18nKeys';
import {ChatMessages} from "../../../../../../components/Chat/ChatMessages";
/*
export function generateStaticParams({
params: { locale },
}: { params: { locale: string } }) {
@ -15,56 +16,22 @@ export function generateStaticParams({
return result;
}
*
*/
export default function Message({ params }: { params: { locale: string, textId: string } }) {
unstable_setRequestLocale(params.locale);
export default function Message({ params: { locale, textId } }: { params: { locale: string, textId: string } }) {
return (
<>
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link href={'/account/messages' as any}>
{i18nText('accountMenu.messages', params.locale)}
{i18nText('accountMenu.messages', locale)}
</Link>
</li>
<li className="breadcrumb-item active" aria-current="page">{`Person ${params.textId}`}</li>
</ol>
<div className="b-message">
<div className="b-message__inner">
<div className="b-message__list b-message__list--me">
<div className="b-message__item ">
<div className="b-message__avatar">
<img src="/images/person.png" className="" alt="" />
</div>
<div className="b-message__text">
🤩 It all for you!
<span className="date">07.09.2022</span>
</div>
</div>
</div>
<div className="b-message__list">
<div className="b-message__item">
<div className="b-message__avatar">
<img src="/images/person.png" className="" alt="" />
</div>
<div className="b-message__text">
🤩 It all for you!
<span className="date">07.09.2022</span>
</div>
</div>
</div>
</div>
<form className="b-message__form" action="">
<textarea placeholder="Type your message here" name="" id="" />
<label className="b-message__upload-file">
<input type="file" required />
</label>
<div className="b-message__microphone" />
<button className="b-message__btn" type="submit" />
</form>
</div>
<ChatMessages locale={locale} groupId={parseInt(textId)} />
</>
);
};

View File

@ -1,12 +1,10 @@
'use client'
import React, { Suspense } from 'react';
import { unstable_setRequestLocale } from 'next-intl/server';
import { Link } from '../../../../../navigation';
import { CustomInput } from '../../../../../components/view/CustomInput';
import { i18nText } from '../../../../../i18nKeys';
import {ChatList} from "../../../../../components/Chat/ChatList";
export default function Messages({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
export default function Messages({ params: { locale } }: { params: { locale: string } }) {
return (
<>
<ol className="breadcrumb">
@ -15,74 +13,7 @@ export default function Messages({ params: { locale } }: { params: { locale: str
<Suspense>
<CustomInput placeholder={i18nText('name', locale)} />
</Suspense>
<div className="messages-session">
<Link
className="card-profile"
href={'1' as any}
>
<div className="card-profile__header">
<div className="card-profile__header__portrait">
<img src="/images/person.png" className="" alt="" />
</div>
<div className="card-profile__header__inner">
<div style={{ width: '100%' }}>
<div className="card-profile__header__name">
David
<span className="count">14</span>
</div>
<div className="card-profile__header__title">
Lorem ipsum dolor sit at, consecte...
</div>
<div className="card-profile__header__date ">
25 may
</div>
</div>
</div>
</div>
</Link>
<Link
className="card-profile"
href={'2' as any}
>
<div className="card-profile__header">
<div className="card-profile__header__portrait">
<img src="/images/person.png" className="" alt="" />
</div>
<div className="card-profile__header__inner">
<div style={{ width: '100%' }}>
<div className="card-profile__header__name">David</div>
<div className="card-profile__header__title">
Lorem ipsum dolor sit at, consecte...
</div>
<div className="card-profile__header__date ">
25 may
</div>
</div>
</div>
</div>
</Link>
<Link
className="card-profile"
href={'3' as any}
>
<div className="card-profile__header">
<div className="card-profile__header__portrait">
<img src="/images/person.png" className="" alt="" />
</div>
<div className="card-profile__header__inner">
<div style={{ width: '100%' }}>
<div className="card-profile__header__name">David</div>
<div className="card-profile__header__title">
Lorem ipsum dolor sit at, consecte...
</div>
<div className="card-profile__header__date ">
25 may
</div>
</div>
</div>
</div>
</Link>
</div>
<ChatList locale={locale}/>
</>
);
};

View File

@ -4,21 +4,44 @@ import { Button } from 'antd';
import { useSelectedLayoutSegment, usePathname } from 'next/navigation';
import { Link } from '../../navigation';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../constants/common';
import { deleteStorageKey } from '../../hooks/useLocalStorage';
import {deleteStorageKey, useLocalStorage} from '../../hooks/useLocalStorage';
import { i18nText } from '../../i18nKeys';
import { getMenuConfig } from '../../utils/account';
import {useEffect, useState} from "react";
import {getChatList} from "../../actions/chat/groups";
export const AccountMenu = ({ locale }: { locale: string }) => {
const selectedLayoutSegment = useSelectedLayoutSegment();
const pathname = selectedLayoutSegment || '';
const paths = usePathname();
const menu: { path: string, title: string, count?: number }[] = getMenuConfig(locale);
const [counts, setCounts] = useState<any>({});
const onLogout = () => {
deleteStorageKey(AUTH_TOKEN_KEY);
deleteStorageKey(AUTH_USER);
window?.location?.replace(`/${paths.split('/')[1]}/`);
};
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
let init = false
useEffect(() => {
if (jwt && locale && !init) {
init = true;
getChatList(locale, jwt).then((payload: any)=> {
if (payload?.directs) {
let summ = 0;
payload?.directs.forEach((el: any) => {
summ = summ + el.newMessagesCount
})
setCounts({'messages': summ})
}
})
}
}, [jwt, locale])
const getterCount = (path: string, count: number)=> {
return counts[path]? counts[path] : count
}
return (
<ul className="list-sidebar">
@ -26,8 +49,8 @@ export const AccountMenu = ({ locale }: { locale: string }) => {
<li key={path} className="list-sidebar__item">
<Link href={`/account/${path}` as any} className={path === pathname ? 'active' : ''}>
{title}
{count ? (
<span className="count">{count}</span>
{getterCount(path, count) ? (
<span className="count">{getterCount(path, count)}</span>
) : null}
</Link>
</li>

View File

@ -0,0 +1,76 @@
'use client'
import React, {useEffect, useState} from 'react';
import {AUTH_TOKEN_KEY} from '../../constants/common';
import {getChatList, getChatMessages} from "../../actions/chat/groups";
import {useLocalStorage} from "../../hooks/useLocalStorage";
import {Link} from "../../navigation";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import {message} from "antd";
import {Loader} from "../view/Loader";
dayjs.extend(relativeTime);
type CompProps = {
locale: string;
};
export const ChatList = ({ locale }: CompProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [chats, setСhats] = useState<any | undefined>();
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
if (jwt) {
setLoading(true);
Promise.all([
getChatList(locale, jwt),
])
.then(([_groups]) => {
setСhats(_groups)
})
.catch((e) => {
console.log(e)
message.error('Не удалось загрузить данные');
})
.finally(() => {
setLoading(false);
})
}
},[jwt])
return (
<div className="messages-session">
<Loader isLoading={loading}>
{chats?.directs.map((item: any, i: number) => (
<Link
key={'chat'+i}
className="card-profile"
href={'messages/'+item.group.id as any}
>
<div className="card-profile__header">
<div className="card-profile__header__portrait">
<img src={item.faceImageUrl} className="" alt="" />
</div>
<div className="card-profile__header__inner">
<div style={{ width: '100%' }}>
<div className="card-profile__header__name">
{item.firstName}
{item.newMessagesCount && (
<span className="count">{item.newMessagesCount}</span>
)}
</div>
<div className="card-profile__header__title">
{item?.lastMessage?.text}
</div>
<div className="card-profile__header__date ">
{dayjs(item?.lastMessage?.sentAt).fromNow()}
</div>
</div>
</div>
</div>
</Link>
))}
</Loader>
</div>
)
}

View File

@ -0,0 +1,200 @@
'use client'
import React, {useEffect, useState} from 'react';
import {AUTH_TOKEN_KEY} from '../../constants/common';
import {getChatList, getChatMessages} from "../../actions/chat/groups";
import {useLocalStorage} from "../../hooks/useLocalStorage";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import {message} from "antd";
import {Loader} from "../view/Loader";
import SignalrConnection from "../../lib/signalr-connection";
import {CheckOutlined} from "@ant-design/icons";
dayjs.extend(relativeTime);
type CompProps = {
locale: string;
groupId: number
};
export const ChatMessages = ({ locale, groupId }: CompProps) => {
const { newMessage, joinChat, readMessages, addListener } = SignalrConnection();
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
//const messages = await getChatMessages(locale, jwt, groupId)
const [loading, setLoading] = useState<boolean>(false);
const [text, setText] = useState('');
const [me, setMe] = useState<any | undefined>();
const [notMe, setNotMe] = useState<any | undefined>();
const [messages, setMessages] = useState<any[]>([]);
useEffect(() => {
if (jwt) {
setLoading(true);
Promise.all([
getChatList(locale, jwt),
getChatMessages(locale, jwt, groupId),
])
.then(([_groups, _messages]) => {
//
const _group = _groups.directs.find( (el: any) => el.group.id === groupId)
if (_group.group.members[0].userId === parseInt(_group.userId)){
setMe(_group.group.members[0].user)
setNotMe(_group.group.members[1].user)
} else {
setMe(_group.group.members[1].user)
setNotMe(_group.group.members[0].user)
}
setMessages(_messages.messages);
})
.catch((e) => {
console.log(e)
message.error('Не удалось загрузить данные');
})
.finally(() => {
setLoading(false);
})
}
},[jwt])
const onConnected = (flag: boolean) =>{
if (flag) {
joinChat(groupId)
readUreaded()
}
}
const readUreaded = () => {
const msgs = [] as number[];
messages.forEach((message: any) => {
if (!message.seen && message.sentByMe === false){
msgs.push(message.id)
}
})
readMessages(msgs)
}
const onReceiveMessage = (payload: any) => {
const _messages = [... messages];
let flag = false;
const msg = {
data : payload.data,
dataType: null,
id: payload.id,
receiverId:null,
seen: false,
senderId: payload.creatorId,
sentAt: payload.createdUtc+'.000Z',
sentByMe: payload.creatorId === me.id,
text: payload.content,
type:"text"
}
_messages.forEach((item: any, i) => {
if (item.id === msg.id){
_messages[i] = msg
flag = true
}
})
//console.log(payload, flag)
if (flag){
setMessages([..._messages]);
} else {
setMessages([msg, ..._messages]);
}
readMessages([msg.id])
}
const onOpponentRead = (payload: any) => {
console.log('onOpponentRead', payload)
const _messages = [... messages] as any[];
_messages.forEach((item: any, i) => {
if (item.id === payload.messageId){
_messages[i].seen = true
}
})
setMessages([..._messages])
}
useEffect(() => {
addListener('onConnected', onConnected)
addListener('ReceiveMessage', onReceiveMessage)
addListener('MessageWasRead', onOpponentRead)
}, [messages, me, setMessages]);
const handleSendMessages = () => {
newMessage({
GroupId: groupId.toString(),
CreatorId: me.id,
Content: text
})
setText('')
}
const onEnterPress = (e: any) => {
if(e.keyCode == 13 && e.shiftKey == false) {
e.preventDefault();
handleSendMessages();
}
}
const handleChangeMessage = (e: any) =>{
const { value } = e.target;
e.preventDefault();
setText(value);
return true
}
const messageRender = (message: any, i:number) => {
const item = message.sentByMe ? me : notMe
const date = dayjs(message.sentAt).fromNow()
const imgSrc = item?.faceImage?.descriptor ? 'http://static.bbuddy.expert/' + item.faceImage.descriptor.split(':')[1] : ''
return (
<div
className={message.sentByMe ? 'b-message__list b-message__list--me' : 'b-message__list'}
key={'message'+i}
>
<div className="b-message__item ">
<div className="b-message__avatar">
{imgSrc && (<img src={imgSrc} className="" alt=""/>)}
</div>
<div className="b-message__text" style={{minWidth: '150px'}}>
{message.text}
<span className="date">
<span className="checks" style={{color: message.seen ? 'green' : 'blue'}}>
<CheckOutlined />
{ message?.seen && (<CheckOutlined style={{marginLeft: '-10px'}} />)}
</span>
{date}
</span>
</div>
</div>
</div>
)
}
return (
<div className="b-message">
<Loader isLoading={loading}>
<div className="b-message__inner">
{messages.map((el, i)=> (messageRender(el, i)))}
</div>
</Loader>
<div className="b-message__form">
<textarea placeholder="Type your message here" onKeyDown={onEnterPress}
onChange={handleChangeMessage} value={text}/>
<button className="b-message__btn" type="submit" onClick={handleSendMessages}/>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
'use client';
import React, { FC } from 'react';
import React, {FC, useEffect, useState} from 'react';
import Image from 'next/image';
import { Tag, Image as AntdImage, Space } from 'antd';
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
@ -10,6 +10,10 @@ import { Locale } from '../../types/locale';
import { CustomRate } from '../view/CustomRate';
import { i18nText } from '../../i18nKeys';
import { FilledYellowButton } from '../view/FilledButton';
import SignalrConnection from "../../lib/signalr-connection";
import {useRouter} from "../../navigation";
import {useLocalStorage} from "../../hooks/useLocalStorage";
import {AUTH_TOKEN_KEY} from "../../constants/common";
type ExpertDetailsProps = {
expert: ExpertDetails;
@ -61,8 +65,35 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
};
export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) => {
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] } } = expert || {};
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] , id, botUserId} } = expert || {};
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const isRus = locale === Locale.ru;
const { joinChatPerson, addListener } = SignalrConnection();
const router = useRouter();
const onConnected = (flag: boolean)=>{
if (flag){
}
}
useEffect(() => {
addListener('onConnected', onConnected)
}, []);
const handleJoinChat = () =>{
joinChatPerson(id).then((res: any) =>{
console.log('RES', res)
router.push(`/account/messages/${res.id}`);
})
}
const handleJoinAIChat = () =>{
joinChatPerson(botUserId).then((res: any) =>{
console.log('RES', res)
router.push(`/account/messages/${res.id}`);
})
}
return (
<>
@ -95,16 +126,29 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
<div className="wrap-btn-prise__text">
{`${sessionCost}`} <span>/ {`${sessionDuration}${isRus ? 'мин' : 'min'}`}</span>
</div>
{jwt && (
<div className="w-100" style={{textAlign: 'end'}}>
<FilledYellowButton
onClick={handleJoinChat}
>{i18nText('chat.join', locale)}</FilledYellowButton>
&nbsp;
{botUserId && (
<FilledYellowButton
onClick={handleJoinAIChat}>{i18nText('chat.joinAI', locale)}</FilledYellowButton>
)}
</div>
)}
</div>
</>
);
};
export const ExpertPractice: FC<ExpertPracticeProps> = ({ themes = [], cases = [], locale }) => {
export const ExpertPractice: FC<ExpertPracticeProps> = ({themes = [], cases = [], locale}) => {
return cases?.length > 0 ? (
<div>
<h3 className="title-h3">{i18nText('successfulCase', locale)}</h3>
{cases?.map(({ id, description, themesGroupIds }) => {
{cases?.map(({id, description, themesGroupIds}) => {
const filtered = themes ? themes.filter(({ id }) => themesGroupIds?.includes(+id)) : [];
return (

View File

@ -146,6 +146,10 @@ export default {
saturday: 'Сб',
addNew: 'Добавить',
mExperiences: 'Управленческий опыт',
chat: {
join: 'начать чат',
joinAI: 'начать чат с ИИ'
},
errors: {
invalidEmail: 'Адрес электронной почты недействителен',
emptyEmail: 'Пожалуйста, введите ваш E-mail',

View File

@ -0,0 +1,81 @@
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
import {AUTH_TOKEN_KEY} from "../constants/common";
import {IHttpConnectionOptions} from "@microsoft/signalr/src/IHttpConnectionOptions";
import {IChatMessage} from "../types/chat";
const URL = 'https://api.bbuddy.expert/api/hubs/chat'
const chatMessageMethodName = 'ReceiveMessage';
const chatUserStatusChangeMethodName = 'chatUserStatusChange';
const chatSeenMethodName = 'MessageWasRead';
const chatTypingMethodName = 'directTyping';
const sendChatTextMethodName = 'SendMessage';
const sendChatTypingMethodName = 'DirectIsTyping';
const serverError = 'Error';
const sendChatSeenMethodName = 'ConfirmMessageRead';
const sendChatMessagesSeenMethodName = 'ConfirmMessagesRead';
const joinChatMethodName = 'JoinChat';
const leaveChatMethodName = 'LeaveChat';
const joinChatsMethodName = 'JoinChats';
const leaveChatsMethodName = 'LeaveChats';
const chatsReceiveMessageMethodName = 'ChatsMessageCreated';
class SignalConnector {
private connection: HubConnection;
private events ={} as any;
static instance: SignalConnector;
constructor() {
const options = {
accessTokenFactory: () => localStorage.getItem(AUTH_TOKEN_KEY)
} as IHttpConnectionOptions;
this.connection = new HubConnectionBuilder()
.withUrl(URL, options)
.withAutomaticReconnect()
.configureLogging(LogLevel.Debug)
.build();
this.connection.start().then(()=>{
this.events?.onConnected(true)
for (const k in this.events) {
if (k != 'onConnected'){
this.connection.on(k, (payload: any) => {
this.events[k](payload);
});
}
}
}).catch(err => console.log('SignalR Error', err));
}
public addListener = (name: string, func: any)=> {
this.events[name] = func;
}
public closeConnection = () =>{
this.connection.stop();
}
public newMessage = (message: IChatMessage) => {
this.connection.invoke(sendChatTextMethodName, message).then(x => console.log('NewMsg',x))
}
public joinChat = (groupId: number) => {
this.connection.invoke(joinChatMethodName, groupId, null).then(x => console.log(joinChatMethodName, x))
}
public joinChatPerson = (accId: number) => {
return this.connection.invoke(joinChatMethodName, 0, accId)
}
public readMessages = (messagesId: number[]) => {
this.connection.invoke(sendChatMessagesSeenMethodName, messagesId).then(x => console.log(sendChatMessagesSeenMethodName, x))
}
public static getInstance(): SignalConnector {
if (!SignalConnector.instance)
SignalConnector.instance = new SignalConnector();
return SignalConnector.instance;
}
}
export default SignalConnector.getInstance;

View File

@ -13,6 +13,8 @@
}
&__inner {
display: flex;
flex-direction: column-reverse;
flex-grow: 1;
height: 0;
position: relative;
@ -94,7 +96,8 @@
align-items: center;
textarea {
width: calc(100% - 136px);
resize: none;
width: calc(100% - 40px);
height: 24px;
padding: 0 8px;
border-radius: 0;

12
src/types/chat.ts Normal file
View File

@ -0,0 +1,12 @@
export interface IChatMessage {
GroupId: string;
CreatorId: number;
Content: string;
}
export interface IChatJoin {
GroupId: string;
CreatorId: number;
Content: string;
}

View File

@ -37,6 +37,7 @@ export type ThemeGroup = {
export interface ExpertItem {
id: number;
botUserId?: number;
name: string;
surname?: string;
faceImageUrl?: string;

View File

@ -5,8 +5,7 @@ import { i18nText } from '../i18nKeys';
const ROUTES = ['sessions', 'notifications', 'support', 'information', 'settings', 'messages', 'expert-profile'];
const COUNTS: Record<string, number> = {
sessions: 12,
notifications: 5,
messages: 113
notifications: 5
};
export const getMenuConfig = (locale: string) => ROUTES.map((path) => ({