diff --git a/.env b/.env index bc0c85f..ebaf4f7 100644 --- a/.env +++ b/.env @@ -1,5 +1,8 @@ NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LVB3LK5pVGxNPeKk4gedt5NW4cb8k7BVXvgOMPTK4x1nnbGTD8BCqDqgInboT6N72YwrTl4tOsVz8rAjbUadX1m00y4Aq5qE8 +STRIPE_SECRET_KEY=sk_test_51LVB3LK5pVGxNPeK6j0wCsPqYMoGfcuwf1LpwGEBsr1dUx4NngukyjYL2oMZer5EOlW3lqnVEPjNDruN0OkUohIf00fWFUHN5O +STRIPE_PAYMENT_DESCRIPTION='BBuddy services' NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc diff --git a/package-lock.json b/package-lock.json index fe4a8ea..3bba170 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,25 @@ { "name": "bbuddy-ui", - "version": "0.0.4", + "version": "0.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bbuddy-ui", - "version": "0.0.4", + "version": "0.2.5", "dependencies": { "@ant-design/cssinjs": "^1.18.1", "@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", + "@stripe/react-stripe-js": "^2.7.3", + "@stripe/stripe-js": "^4.1.0", + "agora-rtc-react": "2.1.0", "agora-rtc-sdk-ng": "^4.20.2", "antd": "^5.12.1", "antd-img-crop": "^4.21.0", + "antd-style": "^3.6.2", "axios": "^1.6.5", "contentful": "^10.13.3", "dayjs": "^1.11.10", @@ -27,7 +30,9 @@ "react-dom": "^18", "react-signalr": "^0.2.24", "react-slick": "^0.29.0", + "react-stripe-js": "^1.1.5", "slick-carousel": "^1.8.1", + "stripe": "^16.2.0", "styled-components": "^6.1.1" }, "devDependencies": { @@ -177,6 +182,82 @@ "react": ">=16.9.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.25.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz", @@ -188,6 +269,60 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@contentful/content-source-maps": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@contentful/content-source-maps/-/content-source-maps-0.11.0.tgz", @@ -228,6 +363,81 @@ "node": ">=10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/css": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", + "license": "MIT", + "dependencies": { + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" + } + }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -246,11 +456,93 @@ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/serialize/node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, "node_modules/@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -430,6 +722,54 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@microsoft/signalr": { "version": "8.0.7", "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.7.tgz", @@ -790,6 +1130,29 @@ "license": "MIT", "peer": true }, + "node_modules/@stripe/react-stripe-js": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.9.0.tgz", + "integrity": "sha512-+/j2g6qKAKuWSurhgRMfdlIdKM+nVVJCy/wl0US2Ccodlqx0WqfIIBhUkeONkCG+V/b+bZzcj4QVa3E/rXtT4Q==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": "^1.44.1 || ^2.0.0 || ^3.0.0 || ^4.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-4.10.0.tgz", + "integrity": "sha512-KrMOL+sH69htCIXCaZ4JluJ35bchuCCznyPyrbN8JXSGQfwBI1SuIEMZNwvy8L8ykj29t6sa5BAAiL7fNoLZ8A==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -814,11 +1177,16 @@ "version": "20.16.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", - "dev": true, "dependencies": { "undici-types": "~6.19.2" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -1030,9 +1398,10 @@ } }, "node_modules/agora-rtc-react": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/agora-rtc-react/-/agora-rtc-react-2.3.0.tgz", - "integrity": "sha512-6D0uvXoZFlwQ/DClceJ1PUCpaHv3ebfMKFOnU0DXbiLpeMeYWM2uyuvfrcDjg4fGf033wPEzXVJHS0wx/miyJw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/agora-rtc-react/-/agora-rtc-react-2.1.0.tgz", + "integrity": "sha512-3FGteA7FG51oK5MusbYNgAcKZaAQK+4sbEz4F0DPzcpDxqNANpocJDqOsmXoUAj5yDBsBZelmagU3abd++6RGA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -1179,6 +1548,26 @@ "react-dom": ">=16.8.0" } }, + "node_modules/antd-style": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/antd-style/-/antd-style-3.7.1.tgz", + "integrity": "sha512-CQOfddVp4aOvBfCepa+Kj2e7ap+2XBINg1Kn2osdE3oQvrD7KJu/K0sfnLcFLkgCJygbxmuazYdWLKb+drPDYA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@babel/runtime": "^7.24.1", + "@emotion/cache": "^11.11.0", + "@emotion/css": "^11.11.2", + "@emotion/react": "^11.11.4", + "@emotion/serialize": "^1.1.3", + "@emotion/utils": "^1.2.1", + "use-merge-value": "^1.2.0" + }, + "peerDependencies": { + "antd": ">=5.8.1", + "react": ">=18" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1462,6 +1851,21 @@ "deep-equal": "^2.0.5" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1567,7 +1971,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -1744,6 +2147,12 @@ "node": ">=18" } }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -1752,6 +2161,22 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2052,6 +2477,15 @@ "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz", "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==" }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -2241,7 +2675,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -2795,6 +3228,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3250,6 +3689,15 @@ "integrity": "sha512-PVH+X8/S9J6XItQgIRLlsrwXUmb/v13wxvcZgqohnnlUZZOEWbWZ07bLsuQ9dEMnNpT9APvBuVV50W5QmDt4pA==", "license": "MIT" }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3269,7 +3717,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3364,6 +3811,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", @@ -3435,7 +3888,6 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, "dependencies": { "hasown": "^2.0.2" }, @@ -3790,12 +4242,30 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-pointer": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", @@ -3896,6 +4366,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4224,7 +4700,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4422,7 +4897,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -4430,6 +4904,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4460,8 +4952,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.11.1", @@ -4483,7 +4974,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -4560,7 +5050,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -5242,8 +5731,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-signalr": { "version": "0.2.24", @@ -5277,6 +5765,68 @@ "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-stripe-js": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/react-stripe-js/-/react-stripe-js-1.1.5.tgz", + "integrity": "sha512-4lIucgf/FZj6Uxvf/TH+QQa/Qi3FXigwN/QY6H7naPyoEfw9LOuTzdgPAmm7aeSXj8nZJXVoigiGzzFZchXjew==", + "license": "MIT", + "dependencies": { + "@stripe/react-stripe-js": "1.7.2", + "@stripe/stripe-js": "1.29.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/react-stripe-js/node_modules/@stripe/react-stripe-js": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.7.2.tgz", + "integrity": "sha512-IAVg2nPUPoSwI//XDRCO7D8mGeK4+N3Xg63fYZHmlfEWAuFVcuaqJKTT67uzIdKYZhHZ/NMdZw/ttz+GOjP/rQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": "^1.26.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/react-stripe-js/node_modules/@stripe/stripe-js": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.29.0.tgz", + "integrity": "sha512-OsUxk0VLlum8E2d6onlEdKuQcvLMs7qTrOXCnl/BGV3fAm65qr6h3e1IZ5AX4lgUlPRrzRcddSOA5DvkKKYLvg==", + "license": "MIT" + }, + "node_modules/react-stripe-js/node_modules/react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" + } + }, + "node_modules/react-stripe-js/node_modules/scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5348,7 +5898,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -5365,7 +5914,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -5673,6 +6221,15 @@ "node": ">=10.0.0" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.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", @@ -5912,6 +6469,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-16.12.0.tgz", + "integrity": "sha512-H7eFVLDxeTNNSn4JTRfL2//LzCbDrMSZ+2q1c7CanVWgK2qIW5TwS+0V7N9KcKZZNpYh/uCqK0PyZh/2UsaAtQ==", + "license": "MIT", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/styled-components": { "version": "6.1.12", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.12.tgz", @@ -6024,7 +6594,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6271,8 +6840,7 @@ "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/universalify": { "version": "0.2.0", @@ -6344,6 +6912,15 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/use-merge-value": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-merge-value/-/use-merge-value-1.2.0.tgz", + "integrity": "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.x" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -6634,6 +7211,15 @@ "node": ">=0.4.0" } }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 64d23ab..ba8255e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bbuddy-ui", - "version": "0.0.4", + "version": "0.2.5", "private": true, "scripts": { "dev": "next dev -p 4200", @@ -14,10 +14,13 @@ "@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", + "@stripe/react-stripe-js": "^2.7.3", + "@stripe/stripe-js": "^4.1.0", + "agora-rtc-react": "2.1.0", "agora-rtc-sdk-ng": "^4.20.2", "antd": "^5.12.1", "antd-img-crop": "^4.21.0", + "antd-style": "^3.6.2", "axios": "^1.6.5", "contentful": "^10.13.3", "dayjs": "^1.11.10", @@ -28,7 +31,9 @@ "react-dom": "^18", "react-signalr": "^0.2.24", "react-slick": "^0.29.0", + "react-stripe-js": "^1.1.5", "slick-carousel": "^1.8.1", + "stripe": "^16.2.0", "styled-components": "^6.1.1" }, "devDependencies": { diff --git a/public/.well-known/apple-app-site-association b/public/.well-known/apple-app-site-association new file mode 100644 index 0000000..90ddeb9 --- /dev/null +++ b/public/.well-known/apple-app-site-association @@ -0,0 +1,11 @@ +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": "GTYAM4FYH3.com.bbuddy.whistle", + "paths": ["/en/experts/*"] + } + ] + } +} \ No newline at end of file diff --git a/public/.well-known/assetlinks.json b/public/.well-known/assetlinks.json new file mode 100644 index 0000000..ddaf1a5 --- /dev/null +++ b/public/.well-known/assetlinks.json @@ -0,0 +1,14 @@ +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.bbuddy.whistle", + "sha256_cert_fingerprints": [ + "87:A2:49:9A:F4:05:9C:06:3C:3D:F3:10:88:F5:49:6D:5F:F2:BC:1E:90:0D:F2:37:A5:BA:37:19:5C:A3:75:C2", + "D0:28:97:E7:64:5D:ED:8D:7F:F1:41:B2:E8:F6:AB:7B:EE:FB:A3:1A:A2:D7:92:D4:C5:41:9A:3C:47:CE:EB:43", + "86:42:FE:EA:44:22:9D:16:7F:FC:70:92:A6:39:9D:B1:C3:F1:DE:21:32:4A:45:8C:07:98:39:55:AF:47:32:66" + ] + } + } +] diff --git a/src/actions/experts.ts b/src/actions/experts.ts index 93aa3fa..3b7cd51 100644 --- a/src/actions/experts.ts +++ b/src/actions/experts.ts @@ -1,5 +1,5 @@ -import { GeneralFilter, ExpertsData, ExpertDetails } from '../types/experts'; import { apiRequest } from './helpers'; +import { GeneralFilter, ExpertsData, ExpertDetails, ExpertScheduler, ExpertSchedulerSession, SignupSessionData } from '../types/experts'; export const getExpertsList = (locale: string, filter?: GeneralFilter): Promise => apiRequest({ url: '/home/coachsearch1', @@ -14,3 +14,18 @@ export const getExpertById = (id: string, locale: string): Promise => apiRequest({ + url: '/home/sessionsignupdata', + method: 'post', + data: { id }, + locale +}); + +export const getSchedulerSession = (data: SignupSessionData, locale: string, token: string): Promise => apiRequest({ + url: '/home/sessionsignupsubmit', + method: 'post', + data, + locale, + token +}); diff --git a/src/actions/hooks/useProfileSettings.ts b/src/actions/hooks/useProfileSettings.ts index bff3878..365d08d 100644 --- a/src/actions/hooks/useProfileSettings.ts +++ b/src/actions/hooks/useProfileSettings.ts @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { ProfileData, ProfileRequest } from '../../types/profile'; import { getPersonalData, setPersonData } from '../profile'; import { useLocalStorage } from '../../hooks/useLocalStorage'; @@ -18,7 +18,7 @@ export const useProfileSettings = (locale: string) => { setProfileSettings(data); }) .catch((err) => { - + console.log(err); }) .finally(() => { setFetchLoading(false); diff --git a/src/actions/hooks/useRoomDetails.ts b/src/actions/hooks/useRoomDetails.ts new file mode 100644 index 0000000..ff3163d --- /dev/null +++ b/src/actions/hooks/useRoomDetails.ts @@ -0,0 +1,51 @@ +'use client' + +import {useCallback, useEffect, useState} from 'react'; +import {useLocalStorage} from '../../hooks/useLocalStorage'; +import {AUTH_TOKEN_KEY} from '../../constants/common'; +import {Room} from '../../types/rooms'; +import {getRoomDetails} from '../rooms'; +import {SessionState} from "../../types/sessions"; + +export const useRoomDetails = (locale: string, roomId: number) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const [room, setRoom] = useState(); + const [errorData, setErrorData] = useState(); + const [loading, setLoading] = useState(false); + const [isStarted, setIsStarted] = useState(false); + + const fetchData = useCallback(() => { + setLoading(true); + setErrorData(undefined); + setRoom(undefined); + + getRoomDetails(locale, jwt, roomId) + .then((room) => { + setRoom(room); + }) + .catch((err) => { + setErrorData(err); + }) + .finally(() => { + setLoading(false); + }) + }, []); + + useEffect(() => { + fetchData(); + }, []); + + useEffect(() => { + if (room?.state === SessionState.STARTED) { + setIsStarted(true); + } + }, [room?.state]) + + return { + fetchData, + loading, + room, + errorData, + isStarted + }; +}; diff --git a/src/actions/rooms.ts b/src/actions/rooms.ts new file mode 100644 index 0000000..c1a8b83 --- /dev/null +++ b/src/actions/rooms.ts @@ -0,0 +1,125 @@ +import { apiRequest } from './helpers'; +import {GetUsersForRooms, Report, ReportData, Room, RoomEdit, RoomEditDTO} from '../types/rooms'; + +export const getUpcomingRooms = (locale: string, token: string): Promise => apiRequest({ + url: '/home/upcomingsessionsall', + method: 'post', + data: { + sessionType: 'room' + }, + locale, + token +}); + +export const getRecentRooms = (locale: string, token: string): Promise => apiRequest({ + url: '/home/historicalmeetings', + method: 'post', + data: { + sessionType: 'room' + }, + locale, + token +}); + +export const getRoomDetails = (locale: string, token: string, id: number): Promise => apiRequest({ + url: '/home/room', + method: 'post', + data: { id }, + locale, + token +}); + +export const deleteRoomClient = (locale: string, token: string, data: { sessionId: number, clientUserId: number }): Promise => apiRequest({ + url: '/home/deleteclientfromroom', + method: 'post', + data, + locale, + token +}); + +export const deleteRoomSupervisor = (locale: string, token: string, data: { sessionId: number, supervisorUserId: number }): Promise => apiRequest({ + url: '/home/deletesupervisorfromroom', + method: 'post', + data, + locale, + token +}); + +export const becomeRoomClient = (locale: string, token: string, data: { sessionId: number, clientUserId: number }): Promise => apiRequest({ + url: '/home/becomeroomclient', + method: 'post', + data, + locale, + token +}); + +export const becomeRoomSupervisor = (locale: string, token: string, data: { sessionId: number, supervisorUserId: number }): Promise => apiRequest({ + url: '/home/becomeroomsupervisor', + method: 'post', + data, + locale, + token +}); + +export const getUsersList = (locale: string, token: string, data: { template: string }): Promise => apiRequest({ + url: '/home/findusersforroom', + method: 'post', + data, + locale, + token +}); + +export const addClient = (locale: string, token: string, data: { sessionId: number, clientUserId: number }): Promise => apiRequest({ + url: '/home/addclienttoroom', + method: 'post', + data, + locale, + token +}); + +export const addSupervisor = (locale: string, token: string, data: { sessionId: number, supervisorUserId: number }): Promise => apiRequest({ + url: '/home/addsupervisortoroom', + method: 'post', + data, + locale, + token +}); + +export const createRoom = (locale: string, token: string): Promise => apiRequest({ + url: '/home/createroom', + method: 'post', + locale, + token +}); + +export const updateRoom = (locale: string, token: string, data: RoomEdit): Promise => apiRequest({ + url: '/home/updateroom', + method: 'post', + data, + locale, + token +}); + +export const getRoomById = (locale: string, token: string, id: number): Promise => apiRequest({ + url: '/home/getroomforedit', + method: 'post', + data: { id }, + locale, + token +}); + +// report +export const getReport = (locale: string, token: string, id: number): Promise => apiRequest({ + url: `/home/getsessionsupervisorscores?sessionId=${id}`, + method: 'post', + locale, + token +}); + +export const saveReport = (locale: string, token: string, data: ReportData): Promise => apiRequest({ + url: '/home/setsessionsupervisorscores', + method: 'post', + data, + locale, + token +}); diff --git a/src/actions/sessions.ts b/src/actions/sessions.ts index 3ae9872..4bdc084 100644 --- a/src/actions/sessions.ts +++ b/src/actions/sessions.ts @@ -91,3 +91,11 @@ export const finishSession = (locale: string, token: string, sessionId: number): locale, token }); + +export const sessionPaymentConfirm = (locale: string, token: string, sessionId: number): Promise => apiRequest({ + url: '/home/session_pay_confirm', + method: 'post', + data: { id: sessionId }, + locale, + token +}); diff --git a/src/actions/stripe.ts b/src/actions/stripe.ts new file mode 100644 index 0000000..989796d --- /dev/null +++ b/src/actions/stripe.ts @@ -0,0 +1,79 @@ +"use server"; + +import { Stripe } from "stripe"; + +import { headers } from "next/headers"; + +import { formatAmountForStripe } from "../utils/stripe-helpers"; +import { stripe } from "../lib/stripe"; + +export async function createCheckoutSession( + data: FormData, +): Promise<{ client_secret: string | null; url: string | null }> { + const ui_mode = data.get( + "uiMode", + ) as Stripe.Checkout.SessionCreateParams.UiMode; + console.log('DATA', data) + const origin: string = headers().get("origin") as string; + + const checkoutSession: Stripe.Checkout.Session = + await stripe.checkout.sessions.create({ + mode: "payment", + submit_type: "donate", + line_items: [ + { + quantity: 1, + price_data: { + currency: 'eur', + product_data: { + name: "Custom amount donation", + }, + unit_amount: formatAmountForStripe( + Number(data.get("customDonation") as string), + 'eur', + ), + }, + }, + ], + ...(ui_mode === "hosted" && { + success_url: `${origin}/payment/with-checkout/result?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${origin}/with-checkout`, + }), + ...(ui_mode === "embedded" && { + return_url: `${origin}/payment/with-embedded-checkout/result?session_id={CHECKOUT_SESSION_ID}`, + }), + ui_mode, + }); + + return { + client_secret: checkoutSession.client_secret, + url: checkoutSession.url, + }; +} + +export async function createPaymentIntent( + data: { amount: number, sessionId?: string }, +): Promise<{ client_secret: string }> { + + const params = { + amount: formatAmountForStripe( + data.amount, + 'eur', + ), + automatic_payment_methods: { enabled: true }, + currency: 'eur', + } as Stripe.PaymentIntentCreateParams; + + if (data?.sessionId){ + params.metadata = { + sessionId : data.sessionId + } + } + + const paymentIntent: Stripe.PaymentIntent = + await stripe.paymentIntents.create(params); + + return { client_secret: paymentIntent.client_secret as string }; +} + +export const getStripePaymentStatus = async (payment_intent: string): Promise => await stripe.paymentIntents.retrieve(payment_intent); \ No newline at end of file diff --git a/src/app/[locale]/(main)/@news/page.tsx b/src/app/[locale]/(main)/@news/page.tsx index 2dadcf5..014a183 100644 --- a/src/app/[locale]/(main)/@news/page.tsx +++ b/src/app/[locale]/(main)/@news/page.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import { useTranslations } from 'next-intl'; -import {getTranslations, unstable_setRequestLocale} from 'next-intl/server'; +// import { useTranslations } from 'next-intl'; +import Link from 'next/link'; +import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; import { i18nText } from '../../../../i18nKeys'; -import {fetchBlogPosts} from "../../../../lib/contentful/blogPosts"; -import Link from "next/link"; +import { fetchBlogPosts } from '../../../../lib/contentful/blogPosts'; export default async function News({params: {locale}}: { params: { locale: string } }) { unstable_setRequestLocale(locale); const t = await getTranslations('Main'); - const {data, total} = await fetchBlogPosts({preview: false, sticky: true}) + const { data, total } = await fetchBlogPosts({preview: false, sticky: true}) return (
diff --git a/src/app/[locale]/(main)/layout.tsx b/src/app/[locale]/(main)/layout.tsx index a22926a..ed99329 100644 --- a/src/app/[locale]/(main)/layout.tsx +++ b/src/app/[locale]/(main)/layout.tsx @@ -15,7 +15,8 @@ import React, { ReactNode } from 'react'; export default function MainLayout({ children, news, experts }: { children: ReactNode, news: ReactNode, - experts: ReactNode + experts: ReactNode, + payment: ReactNode }) { return ( <> @@ -24,4 +25,4 @@ export default function MainLayout({ children, news, experts }: { {experts} ); -}; +} diff --git a/src/app/[locale]/account/(account)/expert-profile/page.tsx b/src/app/[locale]/account/(account)/expert-profile/page.tsx index 7cda5f2..1a46bcf 100644 --- a/src/app/[locale]/account/(account)/expert-profile/page.tsx +++ b/src/app/[locale]/account/(account)/expert-profile/page.tsx @@ -57,7 +57,7 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo } }, [jwt]); - return ( + return data ? ( - ); + ) : null; }; diff --git a/src/app/[locale]/account/(simple)/rooms/[...slug]/page.tsx b/src/app/[locale]/account/(simple)/rooms/[...slug]/page.tsx new file mode 100644 index 0000000..b6f024c --- /dev/null +++ b/src/app/[locale]/account/(simple)/rooms/[...slug]/page.tsx @@ -0,0 +1,57 @@ +import React, { Suspense } from 'react'; +import { unstable_setRequestLocale } from 'next-intl/server'; +import { notFound } from 'next/navigation'; +import { AccountMenu, RoomDetails, RoomsTabs } from '../../../../../../components/Account'; +import { RoomsType } from '../../../../../../types/rooms'; + +const ROOMS_ROUTES = [RoomsType.UPCOMING, RoomsType.RECENT, RoomsType.NEW]; + +export async function generateStaticParams({ + params: { locale }, +}: { params: { locale: string } }) { + return [{ locale, slug: [RoomsType.UPCOMING] }]; +} + +export default function RoomsDetailItem({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) { + unstable_setRequestLocale(locale); + const roomType: string = slug?.length > 0 && slug[0] || ''; + const roomId: number | null = slug?.length > 1 && Number(slug[1]) || null; + + if (!slug?.length || slug?.length > 2) { + notFound(); + } + + if (ROOMS_ROUTES.includes(roomType as RoomsType) && Number.isInteger(roomId)) { + return ( + Loading...

}> + +
+ ); + } + + if (ROOMS_ROUTES.includes(roomType as RoomsType) && !Number.isInteger(roomId)) { + return ( + <> +
+ +
+
+
+ Loading...

}> + +
+
+
+ + ); + } + + return notFound(); +}; diff --git a/src/app/[locale]/account/(simple)/rooms/page.tsx b/src/app/[locale]/account/(simple)/rooms/page.tsx new file mode 100644 index 0000000..b4ed706 --- /dev/null +++ b/src/app/[locale]/account/(simple)/rooms/page.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { redirect } from 'next/navigation'; +import { useLocalStorage } from '../../../../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../../../../constants/common'; +import { RoomsType } from '../../../../../types/rooms'; + +export default function RoomsMainPage() { + const [token] = useLocalStorage(AUTH_TOKEN_KEY, ''); + + return token ? redirect(`rooms/${RoomsType.UPCOMING}`) : null; +}; diff --git a/src/app/[locale]/account/(simple)/sessions/[...slug]/page.tsx b/src/app/[locale]/account/(simple)/sessions/[...slug]/page.tsx index 8a9ed64..59339bf 100644 --- a/src/app/[locale]/account/(simple)/sessions/[...slug]/page.tsx +++ b/src/app/[locale]/account/(simple)/sessions/[...slug]/page.tsx @@ -26,7 +26,7 @@ export default function SessionDetailItem({ params: { locale, slug } }: { params Loading...

}>
diff --git a/src/app/[locale]/bb-expert/page.tsx b/src/app/[locale]/bb-expert/page.tsx index 40c2ede..3cdf5af 100644 --- a/src/app/[locale]/bb-expert/page.tsx +++ b/src/app/[locale]/bb-expert/page.tsx @@ -3,7 +3,7 @@ import type { Metadata } from 'next'; import { unstable_setRequestLocale } from 'next-intl/server'; import { useTranslations } from 'next-intl'; import { GeneralTopSection } from '../../../components/Page'; -import { ScreenCarousel } from '../../../components/Page/ScreenCarousel/index'; +import { ScreenCarousel } from '../../../components/Page/ScreenCarousel'; export const metadata: Metadata = { title: 'Bbuddy - Become a BB expert', diff --git a/src/app/[locale]/blog/page.tsx b/src/app/[locale]/blog/page.tsx index f23d5f1..7569741 100644 --- a/src/app/[locale]/blog/page.tsx +++ b/src/app/[locale]/blog/page.tsx @@ -9,7 +9,6 @@ import {CustomPagination} from "../../../components/view/CustomPagination"; import {DEFAULT_PAGE_SIZE} from "../../../constants/common"; import {BlogPosts} from "../../../components/BlogPosts/BlogPosts"; - interface BlogPostPageParams { slug: string } diff --git a/src/app/[locale]/experts/[expertId]/page.tsx b/src/app/[locale]/experts/[expertId]/page.tsx index 3da5bee..40058c8 100644 --- a/src/app/[locale]/experts/[expertId]/page.tsx +++ b/src/app/[locale]/experts/[expertId]/page.tsx @@ -6,7 +6,6 @@ import { getExpertById, getExpertsList } from '../../../../actions/experts'; import { ExpertCard, ExpertCertificate, - ExpertInformation, ExpertPractice } from '../../../../components/Experts/ExpertDetails'; import { Details } from '../../../../types/education'; @@ -82,8 +81,7 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
- - +

{i18nText('expertBackground', locale)}

diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 3ab1d50..fdb0b55 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -42,4 +42,4 @@ export default function LocaleLayout({ children, params: { locale } }: LayoutPro ); -}; +} diff --git a/src/app/api/webhooks/route.ts b/src/app/api/webhooks/route.ts new file mode 100644 index 0000000..6fa9a39 --- /dev/null +++ b/src/app/api/webhooks/route.ts @@ -0,0 +1,66 @@ +import type { Stripe } from "stripe"; + +import { NextResponse } from "next/server"; + +import { stripe } from "../../../lib/stripe"; + +export async function POST(req: Request) { + let event: Stripe.Event; + + try { + event = stripe.webhooks.constructEvent( + await (await req.blob()).text(), + req.headers.get("stripe-signature") as string, + process.env.STRIPE_WEBHOOK_SECRET as string, + ); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Unknown error"; + // On error, log and return the error message. + if (err! instanceof Error) console.log(err); + console.log(`❌ Error message: ${errorMessage}`); + return NextResponse.json( + { message: `Webhook Error: ${errorMessage}` }, + { status: 400 }, + ); + } + + // Successfully constructed event. + console.log("✅ Success:", event.id); + + const permittedEvents: string[] = [ + "checkout.session.completed", + "payment_intent.succeeded", + "payment_intent.payment_failed", + ]; + + if (permittedEvents.includes(event.type)) { + let data; + + try { + switch (event.type) { + case "checkout.session.completed": + data = event.data.object as Stripe.Checkout.Session; + console.log(`💰 CheckoutSession status: ${data.payment_status}`); + break; + case "payment_intent.payment_failed": + data = event.data.object as Stripe.PaymentIntent; + console.log(`❌ Payment failed: ${data.last_payment_error?.message}`); + break; + case "payment_intent.succeeded": + data = event.data.object as Stripe.PaymentIntent; + console.log(`💰 PaymentIntent status: ${data.status}`); + break; + default: + throw new Error(`Unhandled event: ${event.type}`); + } + } catch (error) { + console.log(error); + return NextResponse.json( + { message: "Webhook handler failed" }, + { status: 500 }, + ); + } + } + // Return a response to acknowledge receipt of the event. + return NextResponse.json({ message: "Received" }, { status: 200 }); +} \ No newline at end of file diff --git a/src/components/Account/ProfileSettings.tsx b/src/components/Account/ProfileSettings.tsx index cfbb04b..2194da7 100644 --- a/src/components/Account/ProfileSettings.tsx +++ b/src/components/Account/ProfileSettings.tsx @@ -1,8 +1,8 @@ 'use client'; import React, { FC, useEffect, useState } from 'react'; -import { Button, Form, message, Upload } from 'antd'; -import type { GetProp, UploadFile, UploadProps } from 'antd'; +import { Form, message, Upload } from 'antd'; +import type { UploadFile } from 'antd'; import ImgCrop from 'antd-img-crop'; import { CameraOutlined, DeleteOutlined } from '@ant-design/icons'; import { useRouter } from '../../navigation'; @@ -12,17 +12,14 @@ import { validateImage } from '../../utils/account'; import { useProfileSettings } from '../../actions/hooks/useProfileSettings'; import { CustomInput } from '../view/CustomInput'; import { OutlinedButton } from '../view/OutlinedButton'; -import {FilledButton, FilledSquareButton, FilledYellowButton} from '../view/FilledButton'; +import { FilledSquareButton, FilledYellowButton } from '../view/FilledButton'; import { DeleteAccountModal } from '../Modals/DeleteAccountModal'; import { Loader } from '../view/Loader'; -import {ButtonProps} from "antd/es/button/button"; type ProfileSettingsProps = { locale: string; }; -type FileType = Parameters>[0]; - export const ProfileSettings: FC = ({ locale }) => { const [form] = Form.useForm(); const { profileSettings, fetchProfileSettings, save, fetchLoading } = useProfileSettings(locale); @@ -58,7 +55,7 @@ export const ProfileSettings: FC = ({ locale }) => { const onSaveProfile = () => { form.validateFields() .then(({ login, surname, username }) => { - const { phone, role, languagesLinks } = profileSettings; + const { phone, role, languagesLinks } = profileSettings || {}; const newProfile: ProfileRequest = { phone, role, @@ -75,7 +72,7 @@ export const ProfileSettings: FC = ({ locale }) => { reader.readAsDataURL(photo as File); reader.onloadend = () => { const newReg = new RegExp('data:image/(png|jpg|jpeg);base64,') - newProfile.faceImage = reader.result.replace(newReg, ''); + newProfile.faceImage = reader?.result?.replace(newReg, ''); newProfile.isFaceImageKeepExisting = false; onSave(newProfile); @@ -181,7 +178,7 @@ export const ProfileSettings: FC = ({ locale }) => { > {i18nText('save', locale)} - router.push('change-password')}> + router.push('settings/change-password')}> {i18nText('changePass', locale)} +

void; +}; + +export const AgoraGroup = ({ roomId, secret, stopCalling }: AgoraProps) => { + const [calling, setCalling] = useState(false); + const [micOn, setMic] = useState(false); + const [cameraOn, setCamera] = useState(false); + + useEffect(() => { + setCalling(true); + }, []); + + useJoin( + { + appid: process.env.NEXT_PUBLIC_AGORA_APPID, + channel: `${roomId}-${secret}`, + token: null, + }, + calling, + ); + + const stop = () => { + stopCalling(); + setCalling(false); + }; + + return ( + <> +
+ +
+
+ setCamera(a => !a)} + setMic={() => setMic(a => !a)} + /> +
+ + ); +}; diff --git a/src/components/Account/agora/components/UsersGroupPanel.tsx b/src/components/Account/agora/components/UsersGroupPanel.tsx new file mode 100644 index 0000000..be7f901 --- /dev/null +++ b/src/components/Account/agora/components/UsersGroupPanel.tsx @@ -0,0 +1,44 @@ +import { + useIsConnected, useLocalCameraTrack, useLocalMicrophoneTrack, usePublish, + useRemoteAudioTracks, + useRemoteUsers, + useRemoteVideoTracks +} from 'agora-rtc-react'; +import { LocalUser } from './LocalUser'; +import { RemoteVideoPlayer } from './RemoteVideoPlayer'; + +type UsersGroupPanelProps = { + calling: boolean; + micOn: boolean; + cameraOn: boolean; +}; + +export const UsersGroupPanel = ({ calling, micOn, cameraOn }: UsersGroupPanelProps) => { + const isConnected = useIsConnected(); + const remoteUsers = useRemoteUsers(); + const { localMicrophoneTrack } = useLocalMicrophoneTrack(micOn); + const { localCameraTrack } = useLocalCameraTrack(cameraOn); + const { videoTracks } = useRemoteVideoTracks(remoteUsers); + const { audioTracks } = useRemoteAudioTracks(remoteUsers); + + usePublish([localMicrophoneTrack, localCameraTrack]); + audioTracks.map(track => track.play()); + + return calling && isConnected && remoteUsers ? ( +
+
+ +
+ {remoteUsers.length > 0 && remoteUsers.map((user) => ( +
+ +
+ ))} +
+ ) : null; +} diff --git a/src/components/Account/agora/components/index.ts b/src/components/Account/agora/components/index.ts index 99a5a9d..5e48733 100644 --- a/src/components/Account/agora/components/index.ts +++ b/src/components/Account/agora/components/index.ts @@ -3,3 +3,4 @@ export * from './UserCover'; export * from './RemoteUsers'; export * from './LocalUserPanel'; export * from './RemoteUserPanel'; +export * from './UsersGroupPanel'; diff --git a/src/components/Account/agora/index.tsx b/src/components/Account/agora/index.tsx index cbca252..1daeaf3 100644 --- a/src/components/Account/agora/index.tsx +++ b/src/components/Account/agora/index.tsx @@ -2,7 +2,9 @@ import AgoraRTC, { AgoraRTCProvider } from 'agora-rtc-react'; import { Session } from '../../../types/sessions'; +import { Room } from '../../../types/rooms'; import { Agora } from './Agora'; +import { AgoraGroup } from './AgoraGroup'; export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Session, stopCalling: () => void, isCoach: boolean }) => { const remoteUser = isCoach ? (session?.clients?.length ? session?.clients[0] : undefined) : session?.coach; @@ -20,3 +22,17 @@ export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Sessi ) : null; }; + +export const AgoraClientGroup = ({ room, stopCalling }: { room?: Room, stopCalling: () => void }) => { + return room ? ( + + {room && ( + + )} + + ) : null; +}; diff --git a/src/components/Account/index.ts b/src/components/Account/index.ts index 8f56865..9ade85d 100644 --- a/src/components/Account/index.ts +++ b/src/components/Account/index.ts @@ -3,3 +3,4 @@ export { AccountMenu } from './AccountMenu'; export { ProfileSettings } from './ProfileSettings'; export * from './sessions'; +export * from './rooms'; diff --git a/src/components/Account/rooms/CreateRoom.tsx b/src/components/Account/rooms/CreateRoom.tsx new file mode 100644 index 0000000..971305e --- /dev/null +++ b/src/components/Account/rooms/CreateRoom.tsx @@ -0,0 +1,45 @@ +'use client' + +import React, { useEffect, useState } from 'react'; +import { EditRoomForm } from './EditRoomForm'; +import debounce from 'lodash/debounce'; +import { createRoom } from '../../../actions/rooms'; +import { Loader } from '../../view/Loader'; +import { useRouter } from '../../../navigation'; +import { RoomsType } from '../../../types/rooms'; + + +export const CreateRoom = ({ locale, jwt }: { locale: string, jwt: string }) => { + const [roomId, setRoomId] = useState(); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + const getRoom = debounce(() => { + createRoom(locale, jwt) + .then((data) => { + setRoomId(data); + }) + .finally(() => { + setLoading(false); + }) + }, 500); + + useEffect(() => { + setLoading(true); + getRoom(); + }, []); + + return ( + + {roomId && ( + router.push(`/account/rooms/${RoomsType.UPCOMING}`)} + /> + )} + + ) +}; diff --git a/src/components/Account/rooms/EditRoomForm.tsx b/src/components/Account/rooms/EditRoomForm.tsx new file mode 100644 index 0000000..899f7d0 --- /dev/null +++ b/src/components/Account/rooms/EditRoomForm.tsx @@ -0,0 +1,220 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Button, Form, Input, notification } from 'antd'; +import dayjs, { Dayjs } from 'dayjs'; +import { i18nText } from '../../../i18nKeys'; +import { Tag } from '../../../types/tags'; +import { Slot } from '../../../types/experts'; +import { RoomEdit, RoomEditDTO } from '../../../types/rooms'; +import { getRoomById, updateRoom } from '../../../actions/rooms'; +import { Loader } from '../../view/Loader'; +import { CustomInput } from '../../view/CustomInput'; +import { CustomSelect } from '../../view/CustomSelect'; +import { CustomSwitch } from '../../view/CustomSwitch'; +import { CustomMultiSelect } from '../../view/CustomMultiSelect'; +import { CustomDatePicker } from '../../view/CustomDatePicker'; + +type EditRoomFormProps = { + roomId: number, + locale: string, + jwt: string, + mode: 'create' | 'edit'; + afterSubmit?: () => void; +} + +type RoomFormState = { + title?: string; + description?: string; + date?: Dayjs; + maxCount?: number; + startAt?: string; + supervisor?: boolean; + tags?: number[]; +}; + +export const EditRoomForm = ({ roomId, locale, jwt, mode, afterSubmit }: EditRoomFormProps) => { + const [form] = Form.useForm(); + const [editingRoom, setEditingRoom] = useState(); + const dateValue = Form.useWatch('date', form); + const [loading, setLoading] = useState(false); + const [fetchLoading, setFetchLoading] = useState(false); + + useEffect(() => { + setFetchLoading(true); + getRoomById(locale, jwt, roomId) + .then((data) => { + setEditingRoom(data); + const { item } = data || {}; + + if (mode === 'edit' && item) { + form.setFieldsValue({ + title: item.title, + description: item.description, + date: item?.scheduledStartAtUtc ? dayjs(item.scheduledStartAtUtc) : undefined, + maxCount: item.maxClients, + startAt: item?.scheduledStartAtUtc, + supervisor: item.isNeedSupervisor, + tags: item.tagIds || undefined + }) + } + }) + .finally(() => { + setFetchLoading(false); + }) + }, []); + + const getAvailableSlots = useCallback((): string[] => { + const dateList = new Set(); + if (editingRoom?.availableSlots) { + editingRoom.availableSlots.forEach(({ startTime }) => { + const [date] = startTime.split('T'); + dateList.add(dayjs(date).format('YYYY-MM-DD')); + }); + + return Array.from(dateList); + } + + return []; + }, [editingRoom?.availableSlots]); + + const getTimeOptions = (slots?: Slot[], curDate?: Dayjs) => { + const date = curDate ? curDate.format('YYYY-MM-DD') : ''; + if (slots && slots?.length && date) { + return slots.filter(({ startTime }) => dayjs(startTime).format('YYYY-MM-DD') === date) + .map(({ startTime, endTime }) => ({ value: startTime, label: `${dayjs(startTime).format('HH:mm')} - ${dayjs(endTime).format('HH:mm')}` })); + } + + return []; + } + + const getTagsOptions = (tags?: Tag[]) => { + if (tags) { + return tags.map(({ id, name }) => ({ value: id, label: {name} })) || []; + } + + return []; + } + + const onSubmit = () => { + setLoading(true); + const { title, description, startAt, maxCount, tags, supervisor } = form.getFieldsValue(); + const result: RoomEdit = { + ...editingRoom, + id: roomId, + title, + scheduledStartAtUtc: startAt, + maxClients: maxCount, + isNeedSupervisor: supervisor, + tagIds: tags || [] + }; + + if (description) { + result.description = description; + } + + updateRoom(locale, jwt, result) + .then(() => { + afterSubmit && afterSubmit(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }) + .finally(() => { + setLoading(false) + }); + } + + const disabledDate = (current: Dayjs) => current && !getAvailableSlots().includes(current.format('YYYY-MM-DD')); + + return ( + +
+ + + + + + +
+ + + + + + + + ({ value: i+1, label: i+1 }))} + /> + + + + +
+ + + + +
+
+ ); +}; diff --git a/src/components/Account/rooms/RoomDetails.tsx b/src/components/Account/rooms/RoomDetails.tsx new file mode 100644 index 0000000..cf3c0d2 --- /dev/null +++ b/src/components/Account/rooms/RoomDetails.tsx @@ -0,0 +1,88 @@ +'use client' + +import React, { useEffect, useState } from 'react'; +import { RoomsType } from '../../../types/rooms'; +import { useSessionTracking } from '../../../actions/hooks/useSessionTracking'; +import { AccountMenu } from '../AccountMenu'; +import { Loader } from '../../view/Loader'; +import { RoomDetailsContent } from './RoomDetailsContent'; +import { useRoomDetails } from '../../../actions/hooks/useRoomDetails'; +import { AgoraClientGroup } from '../agora'; +import { SupervisorReportModal } from '../../Modals/SupervisorReportModal'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { AUTH_USER } from '../../../constants/common'; + +type RoomDetailsProps = { + locale: string; + roomId: number; + activeType: RoomsType; +}; + +export const RoomDetails = ({ roomId, locale, activeType }: RoomDetailsProps) => { + const { room, errorData, loading, fetchData, isStarted } = useRoomDetails(locale, roomId); + const tracking = useSessionTracking(locale, roomId); + const [isCalling, setIsCalling] = useState(false); + const [isOpenReport, setIsOpenReport] = useState(false); + const [userData] = useLocalStorage(AUTH_USER, ''); + const { id: userId = 0 } = userData ? JSON.parse(userData) : {}; + const isSupervisor = room?.supervisor && room.supervisor.id === +userId || false; + + useEffect(() => { + if (isCalling) { + tracking.start(); + } else { + tracking.stop(); + } + }, [isCalling]); + + useEffect(() => { + if (isSupervisor && isStarted) { + setIsOpenReport(true); + } + }, [isStarted]); + + const stopCalling = () => { + setIsCalling(false); + fetchData(); + } + + return isCalling + ? ( + + ) : ( + <> +
+ +
+
+
+ + setIsCalling(true)} + refresh={fetchData} + /> + +
+ {isSupervisor && room?.id && ( + setIsOpenReport(false)} + locale={locale} + refresh={fetchData} + roomId={room.id} + /> + )} +
+ + ); +}; diff --git a/src/components/Account/rooms/RoomDetailsContent.tsx b/src/components/Account/rooms/RoomDetailsContent.tsx new file mode 100644 index 0000000..ade0e89 --- /dev/null +++ b/src/components/Account/rooms/RoomDetailsContent.tsx @@ -0,0 +1,377 @@ +'use client' + +import React, { useState, useEffect } from 'react'; +import { Button, notification, Tag } from 'antd'; +import { DeleteOutlined, LeftOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; +import Image from 'next/image'; +import { useRouter } from '../../../navigation'; +import { Report, Room, RoomsType } from '../../../types/rooms'; +import { i18nText } from '../../../i18nKeys'; +import { LinkButton } from '../../view/LinkButton'; +import { + addClient, + addSupervisor, + becomeRoomClient, + becomeRoomSupervisor, + deleteRoomClient, + deleteRoomSupervisor, + getReport +} from '../../../actions/rooms'; +import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { UserListModal } from '../../Modals/UsersListModal'; +import { SessionState } from '../../../types/sessions'; +import { EditRoomForm } from './EditRoomForm'; + +type RoomDetailsContentProps = { + locale: string; + activeType: RoomsType; + room?: Room; + startRoom: () => void; + refresh: () => void; +}; + +export const RoomDetailsContent = ({ room, startRoom, locale, activeType, refresh }: RoomDetailsContentProps) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const [userData] = useLocalStorage(AUTH_USER, ''); + const { id: userId = 0 } = userData ? JSON.parse(userData) : {}; + const router = useRouter(); + const [showModal, setShowModal] = useState(false); + const [forSupervisor, setForSupervisor] = useState(false); + const startDate = room?.scheduledStartAtUtc ? dayjs(room?.scheduledStartAtUtc).locale(locale) : null; + const endDate = room?.scheduledEndAtUtc ? dayjs(room?.scheduledEndAtUtc).locale(locale) : null; + const today = startDate ? dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD') : false; + const isCreator = room?.coach && room.coach.id === +userId || false; + const isSupervisor = room?.supervisor && room.supervisor.id === +userId || false; + const isClient = room?.clients && room.clients.length > 0 && room.clients.map(({ id }) => id).includes(+userId) || false; + const isTimeBeforeStart = room?.scheduledStartAtUtc ? dayjs() < dayjs(room.scheduledStartAtUtc) : false; + const [isEdit, setIsEdit] = useState(false); + const [report, setReport] = useState(); + + useEffect(() => { + if (room?.id && room?.supervisor && activeType === RoomsType.RECENT) { + getReport(locale, jwt, room.id) + .then((data) => { + setReport(data); + }) + } + }, [room]) + + const goBack = () => router.push(`/account/rooms/${activeType}`); + + const checkUserApply = (): boolean => (!room?.supervisor || !isSupervisor) && (!room?.clients || room?.clients && room?.clients.length === 0 || !isClient); + + const deleteClient = (clientUserId: number) => { + if (room?.id) { + deleteRoomClient(locale, jwt, { sessionId: room.id, clientUserId }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }); + } + }; + + const deleteSupervisor = (supervisorUserId?: number) => { + if (room?.id && supervisorUserId) { + deleteRoomSupervisor(locale, jwt, { sessionId: room.id, supervisorUserId }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }) + } + }; + + const becomeClient = () => { + if (room?.id && userId) { + becomeRoomClient(locale, jwt, { sessionId: room.id, clientUserId: +userId }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }); + } + }; + + const becomeSupervisor = () => { + if (room?.id && userId) { + becomeRoomSupervisor(locale, jwt, { sessionId: room.id, supervisorUserId: +userId }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }); + } + }; + + const onInviteSupervisor = () => { + setForSupervisor(true) + setShowModal(true); + }; + + const onAddUser = (id: number) => { + if (room?.id) { + setShowModal(false); + + if (forSupervisor) { + addSupervisor(locale, jwt, { sessionId: room.id, supervisorUserId: id }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }); + } else { + addClient(locale, jwt, { sessionId: room.id, clientUserId: id }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }); + } + } + }; + + const afterEditing = () => { + setIsEdit(false); + refresh(); + } + + return !isEdit ? ( +
+
+ +
+
{room?.title || ''}
+
+ {today + ? `${i18nText('today', locale)} ${startDate?.format('HH:mm')} - ${endDate?.format('HH:mm')}` + : `${startDate?.format('D MMMM')} ${startDate?.format('HH:mm')} - ${endDate?.format('HH:mm')}`} +
+ {room?.themesTags && room.themesTags.length > 0 && ( +
+
+ {room.themesTags.map((skill) => {skill?.name})} +
+
+ )} + {room?.description &&
{room.description}
} + {activeType === RoomsType.UPCOMING && (isCreator || isSupervisor || isClient) && ( +
+ {(isCreator || isClient || isSupervisor) && ( + + )} + {isCreator && isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && ( + + )} +
+ )} +
+
+
{i18nText('room.roomCreator', locale)}
+
+
+
+
+ +
+
+
{`${room?.coach?.name} ${room?.coach?.surname || ''}`}
+
+
+
+
+ {room?.isNeedSupervisor && ( +
+
+
{i18nText('room.supervisor', locale)}
+
+ {room?.supervisor && ( +
+
+
+ +
+
+
{`${room?.supervisor?.name} ${room?.supervisor?.surname || ''}`}
+
+ {isCreator && activeType === RoomsType.UPCOMING && isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && ( + } + onClick={() => deleteSupervisor(room?.supervisor?.id)} + /> + )} +
+
+ )} + {room?.supervisor && activeType === RoomsType.RECENT && ( + <> + {room?.supervisorComment && ( +
{room.supervisorComment}
+ )} + {report && report.length > 0 && ( +
+ {report.map(({ key, score }) => ( +
+
{i18nText(`room.rating_${key?.toLowerCase()}`, locale)}
+
+
{score || 0}
+
+ ))} +
+ )} + + )} + {isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !room?.supervisor && isCreator && activeType === RoomsType.UPCOMING && ( + + )} + {isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !room?.supervisor && !isCreator && activeType === RoomsType.UPCOMING && checkUserApply() && ( + + )} + {!room?.supervisor && !isCreator && !checkUserApply() && ( +
{i18nText('noData', locale)}
+ )} +
+ )} +
+
+
{i18nText('room.participants', locale)}
+
{`${room?.clients?.length || 0}/${room?.maxClients}`}
+
+ {room?.clients && room?.clients?.length > 0 && ( +
+ {room.clients.map(({id, faceImageUrl, name, surname}) => ( +
+
+ +
+
+
{`${name} ${surname || ''}`}
+
+ {isCreator && room?.state === SessionState.COACH_APPROVED && activeType === RoomsType.UPCOMING && isTimeBeforeStart && ( + } + onClick={() => deleteClient(id)} + /> + )} +
+ ))} +
+ )} + {isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && isCreator && activeType === RoomsType.UPCOMING && (!room?.clients || (room?.clients && room?.clients?.length < room.maxClients)) && ( + + )} + {isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !isCreator && activeType === RoomsType.UPCOMING && (!room?.clients || (room?.clients && room?.clients?.length < room.maxClients)) && checkUserApply() && ( + + )} +
+ {room && ( + setShowModal(false)} + submit={onAddUser} + afterCloseModal={() => setForSupervisor(false)} + room={room} + /> + )} +
+ ) : ( +
+
+ +
+ +
+ ); +}; diff --git a/src/components/Account/rooms/RoomsTabs.tsx b/src/components/Account/rooms/RoomsTabs.tsx new file mode 100644 index 0000000..8f4dd7d --- /dev/null +++ b/src/components/Account/rooms/RoomsTabs.tsx @@ -0,0 +1,173 @@ +'use client'; + +import React, { MouseEvent, useCallback, useEffect, useState } from 'react'; +import { Empty, Space } from 'antd'; +import dayjs from 'dayjs'; +import 'dayjs/locale/ru'; +import 'dayjs/locale/en'; +import 'dayjs/locale/de'; +import 'dayjs/locale/it'; +import 'dayjs/locale/fr'; +import 'dayjs/locale/es'; +import { RoomsType } from '../../../types/rooms'; +import { getRecentRooms, getUpcomingRooms } from '../../../actions/rooms'; +import { Loader } from '../../view/Loader'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../../constants/common'; +import { usePathname, useRouter } from '../../../navigation'; +import { i18nText } from '../../../i18nKeys'; +import { CreateRoom } from './CreateRoom'; + +type RoomsTabsProps = { + locale: string; + activeTab: RoomsType; +}; + +export const RoomsTabs = ({ locale, activeTab }: RoomsTabsProps) => { + const [sort, setSort] = useState(); + const [rooms, setRooms] = useState(); + const [loading, setLoading] = useState(true); + const [errorData, setErrorData] = useState(); + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const router = useRouter(); + const pathname = usePathname(); + + const fetchData = () => { + setErrorData(undefined); + setLoading(true); + Promise.all([ + getUpcomingRooms(locale, jwt), + getRecentRooms(locale, jwt) + ]) + .then(([upcoming, recent]) => { + setRooms({ + [RoomsType.UPCOMING]: upcoming || [], + [RoomsType.RECENT]: recent || [] + }); + }) + .catch((err) => { + setErrorData(err); + }) + .finally(() => { + setLoading(false); + }); + }; + + useEffect(() => { + fetchData(); + }, []); + + const onChangeSort = useCallback((value: string) => { + setSort(value); + }, [sort]); + + const onClickSession = (event: MouseEvent, id: number) => { + event.stopPropagation(); + event.preventDefault(); + router.push(`${pathname}/${id}`); + }; + + const getChildren = (list?: any[]) => ( + <> + {/*
+
+ +
+
*/} +
+ {list && list?.length > 0 ? list?.map(({ id, scheduledStartAtUtc, scheduledEndAtUtc, title, coach, clients, supervisor, maxClients }) => { + const startDate = dayjs(scheduledStartAtUtc).locale(locale); + const endDate = dayjs(scheduledEndAtUtc).locale(locale); + const today = dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD'); + + return ( +
) => onClickSession(e, id)}> +
+
+ +
+
+
+
{`${coach?.name} ${coach?.surname || ''}`}
+
{title}
+
+ {today + ? `${i18nText('today', locale)} ${startDate.format('HH:mm')} - ${endDate.format('HH:mm')}` + : `${startDate.format('D MMMM')} ${startDate.format('HH:mm')} - ${endDate.format('HH:mm')}`} +
+
+ {supervisor && ( + <> +
{i18nText('room.supervisor', locale)}
+
{`${supervisor?.name} ${supervisor?.surname || ''}`}
+ + )} +
{i18nText('room.members', locale)}
+
{`${clients.length}/${maxClients}`}
+
+
+
+
+
+ ) + }) : ( + + )} +
+ + ); + + const tabs = [ + { + key: RoomsType.UPCOMING, + label: ( + <> + {i18nText('room.upcoming', locale)} + {rooms?.upcoming && rooms?.upcoming?.length > 0 ? ({rooms?.upcoming.length}) : null} + + ), + children: getChildren(rooms?.upcoming) + }, + { + key: RoomsType.RECENT, + label: i18nText('room.recent', locale), + children: getChildren(rooms?.recent) + }, + { + key: RoomsType.NEW, + label: i18nText('room.newRoom', locale), + children: + } + ]; + + return ( + +
+ {tabs.map(({ key, label }) => ( + router.push(`/account/rooms/${key}`)} + > + {label} + + ))} +
+ {tabs.filter(({ key }) => key === activeTab)[0].children} +
+ ); +}; diff --git a/src/components/Account/rooms/index.tsx b/src/components/Account/rooms/index.tsx new file mode 100644 index 0000000..4441047 --- /dev/null +++ b/src/components/Account/rooms/index.tsx @@ -0,0 +1,6 @@ +'use client' + +export * from './RoomDetails'; +export * from './RoomsTabs'; +export * from './RoomDetailsContent'; +export * from './CreateRoom'; diff --git a/src/components/Account/sessions/SessionDetailsContent.tsx b/src/components/Account/sessions/SessionDetailsContent.tsx index f32622e..212b15c 100644 --- a/src/components/Account/sessions/SessionDetailsContent.tsx +++ b/src/components/Account/sessions/SessionDetailsContent.tsx @@ -81,7 +81,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio const CoachCard = (coach?: PublicUser) => coach ? (
- +
@@ -106,7 +106,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
{session?.themesTags?.slice(0, 2).map((skill) => {skill?.name})} - {session?.themesTags?.length > 2 + {session?.themesTags && session?.themesTags?.length > 2 ? ( @@ -128,7 +128,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio const StudentCard = (student?: PublicUser | null) => student ? (
- +
{`${student?.name} ${student?.surname || ''}`}
diff --git a/src/components/Account/sessions/SessionsTabs.tsx b/src/components/Account/sessions/SessionsTabs.tsx index 29e54b0..73fbd84 100644 --- a/src/components/Account/sessions/SessionsTabs.tsx +++ b/src/components/Account/sessions/SessionsTabs.tsx @@ -14,7 +14,7 @@ import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common'; import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../../actions/sessions'; import { Session, Sessions, SessionType } from '../../../types/sessions'; -import { useRouter } from '../../../navigation'; +import { useRouter, usePathname } from '../../../navigation'; import { i18nText } from '../../../i18nKeys'; type SessionsTabsProps = { @@ -31,6 +31,7 @@ export const SessionsTabs = ({ locale, activeTab }: SessionsTabsProps) => { const [userData] = useLocalStorage(AUTH_USER, ''); const { id: userId = 0 } = userData ? JSON.parse(userData) : {}; const router = useRouter(); + const pathname = usePathname(); const fetchData = () => { setErrorData(undefined); @@ -66,7 +67,7 @@ export const SessionsTabs = ({ locale, activeTab }: SessionsTabsProps) => { const onClickSession = (event: MouseEvent, id: number) => { event.stopPropagation(); event.preventDefault(); - router.push(`${id}`); + router.push(`${pathname}/${id}`); }; const getChildren = (list?: Session[]) => ( diff --git a/src/components/ExpertProfile/ExpertProfile.tsx b/src/components/ExpertProfile/ExpertProfile.tsx index c7e1e71..eb7b78f 100644 --- a/src/components/ExpertProfile/ExpertProfile.tsx +++ b/src/components/ExpertProfile/ExpertProfile.tsx @@ -1,7 +1,7 @@ 'use client' import React, { useState } from 'react'; -import {Alert, message} from 'antd'; +import { Alert, message } from 'antd'; import Image from 'next/image'; import { i18nText } from '../../i18nKeys'; import { ExpertData, PayInfo, ProfileData } from '../../types/profile'; diff --git a/src/components/Experts/AdditionalFilter.tsx b/src/components/Experts/AdditionalFilter.tsx index 5b692ce..383c12d 100644 --- a/src/components/Experts/AdditionalFilter.tsx +++ b/src/components/Experts/AdditionalFilter.tsx @@ -55,8 +55,6 @@ export const ExpertsAdditionalFilter = ({ }; const search = getSearchParamsString(newFilter); - console.log('here1'); - router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`); // router.push({ diff --git a/src/components/Experts/ExpertDetails.tsx b/src/components/Experts/ExpertDetails.tsx index e951016..7fe4283 100644 --- a/src/components/Experts/ExpertDetails.tsx +++ b/src/components/Experts/ExpertDetails.tsx @@ -1,23 +1,28 @@ 'use client'; -import React, {FC, useEffect, useState} from 'react'; +import React, { FC, useState, useEffect } from 'react'; import Image from 'next/image'; -import { Tag, Image as AntdImage, Space } from 'antd'; +import { Tag, Image as AntdImage, Space, Button } from 'antd'; import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons'; +import { SignupSessionData } from '../../types/experts'; import { ExpertDetails, Practice, ThemeGroup } from '../../types/experts'; import { ExpertDocument } from '../../types/file'; import { Locale } from '../../types/locale'; import { CustomRate } from '../view/CustomRate'; import { i18nText } from '../../i18nKeys'; import { FilledYellowButton } from '../view/FilledButton'; +import { getStorageValue } from '../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY, SESSION_DATA } from '../../constants/common'; +import { ScheduleModal } from '../Modals/ScheduleModal'; +import { ScheduleModalResult } from '../Modals/ScheduleModalResult'; 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; locale?: string; + expertId?: string; }; type ExpertPracticeProps = { @@ -26,48 +31,13 @@ type ExpertPracticeProps = { locale?: string; }; -export const ExpertCard: FC = ({ expert, locale }) => { +export const ExpertCard: FC = ({ expert, locale, expertId }) => { const { publicCoachDetails } = expert || {}; - - return ( -
-
-
- -
-
-

{`${publicCoachDetails?.name} ${publicCoachDetails?.surname || ''}`}

-
- {`${publicCoachDetails?.practiceHours} ${i18nText('practiceHours', locale)}`} - | - {`${publicCoachDetails?.supervisionPerYearId} ${i18nText('supervisionCount', locale)}`} -
-
- } disabled /> - {`4/5 (${i18nText('outOf', locale)} 345)`} -
-
-
- -
- ); -}; - -export const ExpertInformation: FC = ({ expert, locale }) => { + const [showSchedulerModal, setShowSchedulerModal] = useState(false); + const [mode, setMode] = useState<'data' | 'time' | 'pay' | 'finish'>('data'); + const isRus = locale === Locale.ru; 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(); @@ -95,8 +65,70 @@ export const ExpertInformation: FC = ({ expert, locale }) => }) } + const checkSession = (data?: SignupSessionData) => { + if (data?.startAtUtc && data?.tagId) { + const jwt = getStorageValue(AUTH_TOKEN_KEY, ''); + sessionStorage?.setItem(SESSION_DATA, JSON.stringify(data)); + if (jwt) { + setMode('pay'); + } else { + setShowSchedulerModal(false); + const showAuth = new Event('show_auth_enter'); + document.dispatchEvent(showAuth); + } + } + } + + const handleShowPayForm = () => { + setShowSchedulerModal(true); + setMode('pay'); + } + + useEffect(() => { + document.addEventListener('show_pay_form', handleShowPayForm); + return () => { + document.removeEventListener('show_pay_form', handleShowPayForm); + }; + }, []); + + const onSchedulerHandle = () => { + setMode('data'); + setShowSchedulerModal(true); + }; + return ( <> +
+
+
+ +
+
+

{`${publicCoachDetails?.name} ${publicCoachDetails?.surname || ''}`}

+
+ {`${publicCoachDetails?.practiceHours} ${i18nText('practiceHours', locale)}`} + | + {`${publicCoachDetails?.supervisionPerYearId} ${i18nText('supervisionCount', locale)}`} +
+
+ } disabled /> + {`4/5 (${i18nText('outOf', locale)} 345)`} +
+
+
+
+ + {/* + + + Video + + */} +
+
{/*

{}

*/}
@@ -122,7 +154,7 @@ export const ExpertInformation: FC = ({ expert, locale }) => {tags?.map((skill) => {skill?.name})}
- console.log('schedule')}>{i18nText('signUp', locale)} + {i18nText('signUp', locale)}
{`${sessionCost}€`} / {`${sessionDuration}${isRus ? 'мин' : 'min'}`}
@@ -140,15 +172,26 @@ export const ExpertInformation: FC = ({ expert, locale }) =>
)}
+ setShowSchedulerModal(false)} + updateMode={setMode} + mode={mode} + expertId={expertId as string} + locale={locale as string} + sessionCost={sessionCost} + checkSession={checkSession} + /> + ); }; -export const ExpertPractice: FC = ({themes = [], cases = [], locale}) => { +export const ExpertPractice: FC = ({ themes = [], cases = [], locale }) => { return cases?.length > 0 ? (

{i18nText('successfulCase', locale)}

- {cases?.map(({id, description, themesGroupIds}) => { + {cases?.map(({ id, description, themesGroupIds }) => { const filtered = themes ? themes.filter(({ id }) => themesGroupIds?.includes(+id)) : []; return ( diff --git a/src/components/Experts/Filter.tsx b/src/components/Experts/Filter.tsx index 846dd34..debb1d1 100644 --- a/src/components/Experts/Filter.tsx +++ b/src/components/Experts/Filter.tsx @@ -114,7 +114,6 @@ export const ExpertsFilter = ({ ...getObjectByAdditionalFilter(searchParams) }; const search = getSearchParamsString(newFilter); - console.log('basePath', basePath); router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`); diff --git a/src/components/Modals/EditExpertEducationModal.tsx b/src/components/Modals/EditExpertEducationModal.tsx index e2720e5..5d52663 100644 --- a/src/components/Modals/EditExpertEducationModal.tsx +++ b/src/components/Modals/EditExpertEducationModal.tsx @@ -1,20 +1,20 @@ 'use client'; -import React, { FC, useEffect, useState } from 'react'; -import {Modal, Button, message, Form, Collapse, GetProp, UploadProps} from 'antd'; +import React, { FC, useState } from 'react'; +import { Modal, Button, message, Form, Collapse } from 'antd'; import type { CollapseProps } from 'antd'; import { CloseOutlined } from '@ant-design/icons'; import { i18nText } from '../../i18nKeys'; -import { PracticePersonData, PracticeDTO, PracticeData, PracticeCase } from '../../types/practice'; +import { PracticePersonData } from '../../types/practice'; import { AUTH_TOKEN_KEY } from '../../constants/common'; import { useLocalStorage } from '../../hooks/useLocalStorage'; -import {setEducation} from '../../actions/profile'; -import {Certificate, Details, EducationData, EducationDTO, Experience} from "../../types/education"; -import {CertificatesContent} from "./educationModalContent/Certificates"; -import {EducationsContent} from "./educationModalContent/Educations"; -import {TrainingsContent} from "./educationModalContent/Trainings"; -import {MbasContent} from "./educationModalContent/Mbas"; -import {ExperiencesContent} from "./educationModalContent/Experiences"; +import { setEducation } from '../../actions/profile'; +import { EducationData, EducationDTO } from '../../types/education'; +import { CertificatesContent } from './educationModalContent/Certificates'; +import { EducationsContent } from './educationModalContent/Educations'; +import { TrainingsContent } from './educationModalContent/Trainings'; +import { MbasContent } from './educationModalContent/Mbas'; +import { ExperiencesContent } from './educationModalContent/Experiences'; type EditExpertEducationModalProps = { open: boolean; diff --git a/src/components/Modals/ScheduleModal.tsx b/src/components/Modals/ScheduleModal.tsx new file mode 100644 index 0000000..5397223 --- /dev/null +++ b/src/components/Modals/ScheduleModal.tsx @@ -0,0 +1,286 @@ +'use client'; + +import React, {FC, useEffect, useState} from 'react'; +import classNames from 'classnames'; +import { Modal, Menu, Calendar, Radio, Button, Input, message, Form } from 'antd'; +import type { CalendarProps, MenuProps } from 'antd'; +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { CloseOutlined } from '@ant-design/icons'; +import dayjs, { Dayjs } from 'dayjs'; +import 'dayjs/locale/ru'; +import 'dayjs/locale/en'; +import 'dayjs/locale/de'; +import 'dayjs/locale/it'; +import 'dayjs/locale/fr'; +import 'dayjs/locale/es'; +import { getLocale } from '../../utils/locale'; +import { AUTH_TOKEN_KEY, SESSION_DATA } from '../../constants/common'; +import { ExpertScheduler, SignupSessionData } from '../../types/experts'; +import { Tag } from '../../types/tags'; +import { getSchedulerByExpertId, getSchedulerSession } from '../../actions/experts'; +import { StripeElementsForm } from '../stripe/StripeElementsForm'; +import { i18nText } from '../../i18nKeys'; +import { CustomSelect } from '../../components/view/CustomSelect'; +import { Loader } from '../view/Loader'; +import { getStorageValue } from '../../hooks/useLocalStorage'; + +type ScheduleModalProps = { + open: boolean; + handleCancel: () => void; + mode: 'data' | 'time' | 'pay' | 'finish'; + updateMode: (mode: 'data' | 'time' | 'pay' | 'finish') => void; + sessionCost: number; + expertId: string; + locale: string; + checkSession: (data?: SignupSessionData) => void; +}; + +type MenuItem = Required['items'][number]; + +const getCalendarMenu = (start: Dayjs): MenuItem[] => Array.from({ length: 3 }) + .map((_: unknown, index: number) => { + const date = index ? start.add(index, 'M') : start.clone(); + return { + label: {date.format('MMMM')}, + key: date.format('YYYY-MM-DD') + } + }); + +export const ScheduleModal: FC = ({ + open, + handleCancel, + mode, + updateMode, + sessionCost, + locale, + expertId, + checkSession, +}) => { + const [selectDate, setSelectDate] = useState(dayjs()); + const [dates, setDates] = useState | undefined>(); + const [tags, setTags] = useState(); + const [rawScheduler, setRawScheduler] = useState(null); + const [isPayLoading, setIsPayLoading] = useState(false); + const [sessionId, setSessionId] = useState(''); + const [form] = Form.useForm<{ clientComment?: string, startAtUtc?: string, tagId?: number }>(); + + dayjs.locale(locale); + + const signupSession = () => { + const data = sessionStorage?.getItem(SESSION_DATA); + const jwt = getStorageValue(AUTH_TOKEN_KEY, ''); + + if (jwt && data) { + const parseData = JSON.parse(data); + setIsPayLoading(true); + getSchedulerSession(parseData as SignupSessionData, locale || 'en', jwt) + .then((session) => { + setSessionId(session?.sessionId); + console.log(session?.sessionId); + }) + .catch((err) => { + console.log(err); + message.error('Не удалось провести оплату') + }) + .finally(() => { + sessionStorage?.removeItem(SESSION_DATA); + setIsPayLoading(false); + }) + } + }; + + useEffect(()=> { + if (open && mode !== 'pay') { + getSchedulerByExpertId(expertId as string, locale as string) + .then((data) => { + setRawScheduler(data); + }) + .catch((err) => { + console.log(err); + }); + } + + if (!open) { + form.resetFields(); + } + }, [open]); + + useEffect(() => { + if (open && mode === 'pay') { + signupSession(); + } + }, [mode]); + + useEffect(() => { + const map = {} as any + rawScheduler?.availableSlots.forEach((el) => { + const key = dayjs(el.startTime).format('YYYY-MM-DD'); + if (!map[key]){ + map[key] = [] + } + map[key].push(el); + }) + setDates(map); + setTags(rawScheduler?.tags) + }, [rawScheduler]); + + const onPanelChange = (value: Dayjs) => setSelectDate(value); + + const onDateChange: CalendarProps['onSelect'] = (value, selectInfo) => { + if (selectInfo.source === 'date') { + setSelectDate(value); + updateMode('time'); + } + }; + + const disabledDate = (currentDate: Dayjs) => !dates || !dates[currentDate.format('YYYY-MM-DD')]; + + const cellRender: CalendarProps['fullCellRender'] = (date, info) => { + const isWeekend = date.day() === 6 || date.day() === 0; + return React.cloneElement(info.originNode, { + ...info.originNode.props, + className: classNames('b-calendar-cell', { + ['b-calendar-cell__select']: selectDate.isSame(date, 'date'), + ['b-calendar-cell__today']: date.isSame(dayjs(), 'date'), + ['b-calendar-cell__weekend']: isWeekend, + }), + children: ( +
+ + {date.get('date')} + +
+ ), + }); + }; + + const onValidate = () => { + form.validateFields() + .then((values) => { + checkSession({ coachId: +expertId, ...values }); + }) + } + + return ( + } + > + {mode === 'data' && ( + { + const start = dayjs().startOf('M'); + const [activeMonth, setActiveMonth] = useState(start.format('YYYY-MM-DD')); + + const onClick: MenuProps['onClick'] = (e) => { + setActiveMonth(e.key); + onChange(dayjs(e.key)); + }; + + return ( + + ); + }} + /> + )} + {mode === 'time' && ( +
+
+ +
+
+
+ {tags && ( + + ({value: id, label: name}))} + /> + + )} +
+
+ + + {dates && dates[selectDate.format('YYYY-MM-DD')].map((el: any) => ( + + {dayjs(el.startTime).format('HH:mm')} - {dayjs(el.endTime).format('HH:mm')} + ) + )} + + +
+ + + +
+ +
+ )} + {mode === 'pay' && ( +
+ + + +
+ )} + + ); +}; diff --git a/src/components/Modals/ScheduleModalResult.tsx b/src/components/Modals/ScheduleModalResult.tsx new file mode 100644 index 0000000..58d97c0 --- /dev/null +++ b/src/components/Modals/ScheduleModalResult.tsx @@ -0,0 +1,73 @@ +'use client' + +import React, { useEffect, useState } from 'react'; +import { Modal, Result } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import { useSearchParams, useRouter } from 'next/navigation'; +import { Stripe } from 'stripe'; +import { getStripePaymentStatus } from '../../actions/stripe'; +import { sessionPaymentConfirm } from '../../actions/sessions'; +import { getStorageValue } from '../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../constants/common'; +import { Session, SessionState } from '../../types/sessions'; +import { i18nText } from '../../i18nKeys'; + +export const ScheduleModalResult = ({ locale }: { locale: string }) => { + const searchParams = useSearchParams(); + const [paymentStatus, setPaymentStatus] = useState(); + const [session, setSession] = useState(); + const [error, setError] = useState(); + const router = useRouter(); + + useEffect(() => { + setError(undefined); + const payment_intent = searchParams.get('payment_intent') || false; + if (payment_intent) { + getStripePaymentStatus(payment_intent) + .then((result) => { + setPaymentStatus(result?.status); + if (result?.status === 'succeeded' && result?.metadata?.sessionId) { + const jwt = getStorageValue(AUTH_TOKEN_KEY, ''); + sessionPaymentConfirm(locale, jwt, +result.metadata.sessionId) + .then((session) => { + setSession(session); + }) + .catch((err: any) => { + setError(err); + }); + } + }) + .catch((err: any) => { + setError(err); + }) + } + }, [searchParams]); + + const onClose = () => { + const { origin, pathname } = window?.location || {}; + + router.push(`${origin}${pathname}`); + setPaymentStatus(undefined); + setSession(undefined); + }; + + return ( + } + > +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/components/Modals/SupervisorReportModal.tsx b/src/components/Modals/SupervisorReportModal.tsx new file mode 100644 index 0000000..f2d5223 --- /dev/null +++ b/src/components/Modals/SupervisorReportModal.tsx @@ -0,0 +1,121 @@ +'use client'; + +import React, { FC, useEffect, useRef, useState } from 'react'; +import { Modal, Button, message, Input } from 'antd'; +import { CloseOutlined, StarFilled } from '@ant-design/icons'; +import { i18nText } from '../../i18nKeys'; +import { useLocalStorage } from '../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../constants/common'; +import { getReport, saveReport } from '../../actions/rooms'; +import { Report, ReportData } from '../../types/rooms'; +import { CustomRate } from '../view/CustomRate'; + +type SupervisorReportModalProps = { + open: boolean; + handleCancel: () => void; + locale: string; + refresh: () => void; + roomId: number; +}; + +export const SupervisorReportModal: FC = ({ + open, + handleCancel, + locale, + roomId, + refresh +}) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const [loading, setLoading] = useState(false); + const [report, setReport] = useState(); + const reasonRef = useRef(null); + + useEffect(() => { + getReport(locale, jwt, roomId) + .then((data) => { + setReport(data); + }) + .catch(() => { + message.error('Не удалось получить отчет'); + }) + }, []); + + const onSaveRate = () => { + const result: ReportData = { + sessionId: roomId, + sessionSupervisorScores: report || [], + supervisorComment: reasonRef?.current?.resizableTextArea?.textArea?.value || '' + }; + + setLoading(true); + saveReport(locale, jwt, result) + .then(() => { + handleCancel(); + refresh(); + }) + .catch(() => { + message.error('Не удалось сохранить отчет'); + }) + .finally(() => { + setLoading(false); + }) + } + + const onChangeRate = (val: number, id: number) => { + setReport(report ? report.map((item) => { + if (item.evaluationCriteriaId === id) { + return { + ...item, + score: val + }; + } + + return item; + }) : undefined); + } + + return ( + } + > +
+
+ {report && report.length > 0 && report.map(({ key, evaluationCriteriaId, score }) => ( +
+
{i18nText(`room.rating_${key?.toLowerCase()}`, locale)}
+ } + onChange={(val: number) => onChangeRate(val, evaluationCriteriaId)} + /> +
+ ))} +
+
+ +
+
+ +
+
+
+ ); +}; diff --git a/src/components/Modals/UsersListModal.tsx b/src/components/Modals/UsersListModal.tsx new file mode 100644 index 0000000..f81443d --- /dev/null +++ b/src/components/Modals/UsersListModal.tsx @@ -0,0 +1,113 @@ +'use client'; + +import React, { useCallback, useState } from 'react'; +import { Button, Modal, notification } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import debounce from 'lodash/debounce'; +import Image from 'next/image'; +import { i18nText } from '../../i18nKeys'; +import { getUsersList } from '../../actions/rooms'; +import { PublicUser } from '../../types/sessions'; +import { Room } from '../../types/rooms'; +import { CustomInput } from '../view/CustomInput'; +import { Loader } from '../view/Loader'; + +type UserListModalProps = { + room: Room; + isOpen: boolean; + locale: string; + handleCancel: () => void; + jwt: string; + submit: (id: number) => void; + afterCloseModal?: () => void; +}; + +export const UserListModal = ({ room, isOpen, locale, handleCancel, jwt, submit, afterCloseModal }: UserListModalProps) => { + const [users, setUsers] = useState(); + const [loading, seLoading] = useState(false); + + const onSearch = useCallback(debounce((e: any) => { + if (e?.target?.value) { + seLoading(true); + getUsersList(locale, jwt, { template: e.target.value }) + .then(({ items }) => { + const clients = room?.clients?.map(({ id }) => id); + setUsers(items + ? items.filter(({ id }) => !(clients?.length && clients.includes(id) || id === room?.supervisor?.id || id === room?.coach?.id)) + : undefined); + }) + .catch((err: any) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }) + .finally(() => { + seLoading(false); + }); + } else { + setUsers(undefined); + } + + }, 300), []); + + const onAfterClose = () => { + setUsers(undefined); + if (afterCloseModal) afterCloseModal(); + } + + return ( + } + afterClose={onAfterClose} + > +
+ + {users && ( +
+ + {users.length > 0 ? ( +
+ {users.map(({ id, name, surname, faceImageUrl }) => ( +
+
+
+ +
+
+
{`${name} ${surname || ''}`}
+
+
+ +
+ ))} +
+ ) : ( +
{i18nText('noData', locale)}
+ )} +
+
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/src/components/Modals/authModalContent/EnterContent.tsx b/src/components/Modals/authModalContent/EnterContent.tsx index 004a776..6bcc377 100644 --- a/src/components/Modals/authModalContent/EnterContent.tsx +++ b/src/components/Modals/authModalContent/EnterContent.tsx @@ -6,7 +6,7 @@ import { AUTH_USER } from '../../../constants/common'; import { SocialConfig } from '../../../constants/social'; import { useOauthWindow } from '../../../hooks/useOauthWindow'; import { getAuth } from '../../../actions/auth'; -import { getPersonalData } from '../../../actions/profile'; +import {getPersonalData, getUserData} from '../../../actions/profile'; import { CustomInput } from '../../view/CustomInput'; import { CustomInputPassword } from '../../view/CustomInputPassword'; import { FilledButton } from '../../view/FilledButton'; @@ -39,7 +39,7 @@ export const EnterContent: FC = ({ getAuth(locale, { login, password }) .then((data) => { if (data.jwtToken) { - getPersonalData(locale, data.jwtToken) + getUserData(locale, data.jwtToken) .then((profile) => { localStorage.setItem(AUTH_USER, JSON.stringify(profile)); updateToken(data.jwtToken); diff --git a/src/components/Page/Header/HeaderAuthLinks.tsx b/src/components/Page/Header/HeaderAuthLinks.tsx index 8d0fc7a..269ce98 100644 --- a/src/components/Page/Header/HeaderAuthLinks.tsx +++ b/src/components/Page/Header/HeaderAuthLinks.tsx @@ -23,6 +23,31 @@ function HeaderAuthLinks ({ const selectedLayoutSegment = useSelectedLayoutSegment(); const pathname = selectedLayoutSegment || ''; const [token, setToken] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const [isPayPath, setIsPayPath] = useState(false); + + const onOpen = (mode: 'enter' | 'register' | 'reset' | 'finish') => { + setMode(mode); + setIsOpenModal(true); + }; + + const handleAuthRegister = () => { + setIsPayPath(true); + onOpen('register'); + }; + + const handleAuthEnter = () => { + setIsPayPath(true); + onOpen('enter'); + }; + + useEffect(() => { + document.addEventListener('show_auth_register', handleAuthRegister); + document.addEventListener('show_auth_enter', handleAuthEnter); + return () => { + document.removeEventListener('show_auth_register', handleAuthRegister); + document.removeEventListener('show_auth_enter', handleAuthEnter); + }; + }, []); useEffect(() => { if (!isOpenModal) { @@ -30,9 +55,16 @@ function HeaderAuthLinks ({ } }, [isOpenModal]); - const onOpen = (mode: 'enter' | 'register' | 'reset' | 'finish') => { - setMode(mode); - setIsOpenModal(true); + useEffect(() => { + if (token && isPayPath) { + const showPayForm = new Event('show_pay_form'); + document.dispatchEvent(showPayForm); + } + }, [token]); + + const addNewEvent = (name: 'show_auth_register' | 'show_auth_enter') => { + const evt = new Event(name); + document.dispatchEvent(evt); }; return token @@ -49,7 +81,7 @@ function HeaderAuthLinks ({ @@ -61,7 +93,7 @@ function HeaderAuthLinks ({ diff --git a/src/components/stripe/StripeElementsForm.tsx b/src/components/stripe/StripeElementsForm.tsx new file mode 100644 index 0000000..004653c --- /dev/null +++ b/src/components/stripe/StripeElementsForm.tsx @@ -0,0 +1,165 @@ +'use client'; + +import React, { FC, useEffect, useState } from 'react'; +import type { StripeError } from '@stripe/stripe-js'; +import { + useStripe, + useElements, + PaymentElement, + Elements, +} from '@stripe/react-stripe-js'; +import { Form, Button, message } from 'antd'; +import getStripe from '../../utils/get-stripe'; +import { createPaymentIntent} from '../../actions/stripe'; +import { Payment } from '../../types/payment'; +import { i18nText } from '../../i18nKeys'; +import { WithError } from '../view/WithError'; + +type PaymentFormProps = { + amount: number, + sessionId?: string, + locale: string +} + +type PaymentInfo = 'initial' | 'error' | 'processing' | 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'succeeded'; + +const PaymentStatus = ({ status }: { status?: PaymentInfo }) => { + switch (status) { + case 'processing': + case 'requires_payment_method': + case 'requires_confirmation': + return

Processing...

; + + case 'requires_action': + return

Authenticating...

; + + case 'succeeded': + return

Payment Succeeded

; + + default: + return null; + } +}; + +export const CheckoutForm: FC = ({ amount, sessionId, locale }) => { + const [form] = Form.useForm(); + const formAmount = Form.useWatch('amount', form); + const [paymentType, setPaymentType] = useState(''); + const [payment, setPayment] = useState<{ + status: PaymentInfo + }>({ status: 'initial' }); + const [errorData, setErrorData] = useState(); + const stripe = useStripe(); + const elements = useElements(); + + useEffect(() => { + elements?.update({ amount: formAmount * 100 }); + }, [formAmount]); + + const onSubmit = async () => { + try { + if (!elements || !stripe) return; + + setErrorData(undefined); + setPayment({ status: "processing" }); + + const { error: submitError } = await elements.submit(); + + if (submitError) { + if (submitError.message) { + message.error(submitError.message); + } + + return; + } + + const { client_secret: clientSecret } = await createPaymentIntent( + { amount, sessionId } + ); + + const { error: confirmError } = await stripe!.confirmPayment({ + elements, + clientSecret, + confirmParams: { + return_url: window.location.href, + payment_method_data: { + allow_redisplay: 'limited', + // billing_details: { + // name: input.cardholderName, + // }, + }, + }, + }); + + if (confirmError) { + setErrorData({ + title: i18nText('errorPayment', locale), + message: confirmError.message ?? 'An unknown error occurred' + }); + } + } catch (err) { + const { message } = err as StripeError; + setErrorData({ + title: i18nText('errorPayment', locale), + message: message ?? 'An unknown error occurred' + }); + } + }; + + return ( + +
+
+ { + setPaymentType(e.value.type); + }} + /> +
+
+ +
+ +
+
+ ); +} + +export const StripeElementsForm: FC = ({ amount, sessionId, locale }) => { + return ( + + + + ); +}; diff --git a/src/components/view/CustomDatePicker.tsx b/src/components/view/CustomDatePicker.tsx new file mode 100644 index 0000000..96361a6 --- /dev/null +++ b/src/components/view/CustomDatePicker.tsx @@ -0,0 +1,60 @@ +'use client' + +import React, { useEffect, useState } from 'react'; +import { DatePicker } from 'antd'; +import { CalendarOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; +import 'dayjs/locale/ru'; +import 'dayjs/locale/en'; +import 'dayjs/locale/de'; +import 'dayjs/locale/it'; +import 'dayjs/locale/fr'; +import 'dayjs/locale/es'; +import { getLocale } from '../../utils/locale'; + +export const CustomDatePicker = (props: any) => { + const { label, value, locale, ...other } = props; + const [isActiveLabel, setIsActiveLabel] = useState(false); + + dayjs.locale(locale); + + useEffect(() => { + if (label) { + setIsActiveLabel(!!value); + } else { + setIsActiveLabel(false); + } + }, [value]); + + const onOpenChange = (open: boolean) => { + if (open) { + if (!isActiveLabel) setIsActiveLabel(true) + } else { + setIsActiveLabel(!!value) + } + }; + + return ( +
+
+ {label} +
+ } + {...other} + /> +
+ ); +}; diff --git a/src/components/view/WithError.tsx b/src/components/view/WithError.tsx index 454a45f..f7b4a3f 100644 --- a/src/components/view/WithError.tsx +++ b/src/components/view/WithError.tsx @@ -16,8 +16,8 @@ export const WithError: FC = ({ return ( Refresh page diff --git a/src/constants/common.ts b/src/constants/common.ts index c5abf3b..ec7be9b 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -1,6 +1,7 @@ export const BASE_URL = process.env.NEXT_PUBLIC_SERVER_BASE_URL || 'https://api.bbuddy.expert/api'; export const AUTH_TOKEN_KEY = 'bbuddy_token'; export const AUTH_USER = 'bbuddy_auth_user'; +export const SESSION_DATA = 'bbuddy_session_data'; export const DEFAULT_PAGE_SIZE = 5; export const DEFAULT_PAGE = 1; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 9221a91..d746b67 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; -function getStorageValue (key: string, defaultValue: any) { +export function getStorageValue (key: string, defaultValue: any) { if (typeof window !== 'undefined') { const saved = localStorage.getItem(key); return saved || defaultValue; diff --git a/src/i18nKeys/de.ts b/src/i18nKeys/de.ts index 3a2f8c0..7134d77 100644 --- a/src/i18nKeys/de.ts +++ b/src/i18nKeys/de.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Kommende & letzte Sitzungen', + rooms: 'Zimmer', notifications: 'Benachrichtigung', support: 'Hilfe & Support', information: 'Rechtliche Informationen', @@ -42,13 +43,47 @@ export default { addComment: 'Neuen Kommentar hinzufügen', commentPlaceholder: 'Ihr Kommentar', clientComments: 'Kundenkommentare', - coachComments: 'Trainerkommentare' + coachComments: 'Expertenkommentare' }, room: { upcoming: 'Zukünftige Räume', requested: 'Angeforderte Räume', recent: 'Kürzliche Räume', - newRoom: 'Neuer Raum' + newRoom: 'Neuer Raum', + editRoom: 'Raum bearbeiten', + date: 'Datum', + time: 'Zeit', + maxParticipants: 'Max. erlaubte Teilnehmer', + presenceOfSupervisor: 'Anwesenheit eines Supervisors', + supervisor: 'Supervisor', + members: 'Mitglieder', + participants: 'Teilnehmer', + roomCreator: 'Raum-Ersteller', + inviteSupervisor: 'Supervisor einladen', + joinSupervisor: 'Als Supervisor beitreten', + inviteParticipant: 'Teilnehmer einladen', + joinParticipant: 'Als Teilnehmer beitreten', + rapport: 'Rapport', + invite: 'Invite', + save: 'Raum speichern', + rate: 'Bewerten', + tellAboutReason: 'Sag uns, was passiert ist', + rating_raport: 'Rapport', + rating_position_and_presence: 'Position oder Präsenz eines Coaches', + rating_balance_and_frustration: 'Balance zwischen Unterstützung und Frustration', + rating_agreement: 'Erstellung einer Coaching-Vereinbarung (Sitzungsvertrag)', + rating_planning_and_goals: 'Planung und Zielsetzung', + rating_reality: 'Klärung der Realität', + rating_opportunities: 'Neue Möglichkeiten gefunden', + rating_action_plan: 'Es wurde ein Aktionsplan erstellt', + rating_motivation: 'Motivationsquellen gefunden', + rating_next_session_stretch: 'Es ist noch Zeit bis zur nächsten Sitzung', + rating_relationship: 'Aufbau einer vertrauensvollen Beziehung zum Klienten', + rating_listening: 'Tiefes, aktives Zuhören', + rating_questions: 'Verwendung „starker“ Fragen', + rating_communication: 'Direkte Kommunikation', + rating_awareness: 'Entwicklung und Stimulierung des Bewusstseins', + rating_progress: 'Fortschritts- und Verantwortungsmanagement' }, agreementText: 'Folgendes habe ich gelesen und erkläre mich damit einverstanden: Benutzervereinbarung,', userAgreement: 'Benutzervereinbarung', @@ -110,9 +145,9 @@ export default { seminars: 'Seminare', courses: 'Kurse', mba: 'MBA-Information', - aboutCoach: 'Über Coach', + aboutCoach: 'Über den Experten', education: 'Bildung', - coaching: 'Coaching', + coaching: 'Expertenprofil', experiences: 'Praktische Erfahrung', payInfo: 'Zahlungsdaten', sessionDuration: 'Sitzungsdauer', @@ -146,6 +181,10 @@ export default { saturday: 'Sa', addNew: 'Neu hinzufügen', mExperiences: 'Führungserfahrung', + pay: 'Zahlung', + sessionWishes: 'Schreiben Sie Ihre Wünsche zur Sitzung', + successPayment: 'Erfolgreiche Zahlung', + errorPayment: 'Zahlungsfehler', errors: { invalidEmail: 'Die E-Mail-Adresse ist ungültig', emptyEmail: 'Bitte geben Sie Ihre E-Mail ein', diff --git a/src/i18nKeys/en.ts b/src/i18nKeys/en.ts index 4be1bc4..fab0990 100644 --- a/src/i18nKeys/en.ts +++ b/src/i18nKeys/en.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Upcoming & Recent Sessions', + rooms: 'Rooms', notifications: 'Notification', support: 'Help & Support', information: 'Legal Information', @@ -42,13 +43,47 @@ export default { addComment: 'Add new', commentPlaceholder: 'Your comment', clientComments: 'Client Comments', - coachComments: 'Coach Comments' + coachComments: 'Expert Comments' }, room: { upcoming: 'Upcoming Rooms', requested: 'Rooms Requested', recent: 'Recent Rooms', - newRoom: 'New Room' + newRoom: 'New Room', + editRoom: 'Edit Room', + date: 'Date', + time: 'Time', + maxParticipants: 'Max Participants Allowed', + presenceOfSupervisor: 'Presence of a Supervisor', + supervisor: 'Supervisor', + members: 'Members', + participants: 'Participants', + roomCreator: 'Room Creator', + inviteSupervisor: 'Invite Supervisor', + joinSupervisor: 'Join As A Supervisor', + inviteParticipant: 'Invite Participant', + joinParticipant: 'Join as a participant', + rapport: 'Rapport', + invite: 'Invite', + save: 'Save room', + rate: 'Rate', + tellAboutReason: 'Tell us what happened', + rating_raport: 'Rapport', + rating_position_and_presence: 'Coaching position or coaching presence', + rating_balance_and_frustration: 'Balance of support and frustration', + rating_agreement: 'Creating a coaching agreement (session contract)', + rating_planning_and_goals: 'Planning and goal setting', + rating_reality: 'Clarifying reality', + rating_opportunities: 'New opportunities found', + rating_action_plan: 'An action plan has been drawn up', + rating_motivation: 'Sources of motivation found', + rating_next_session_stretch: 'There is a stretch for the next session', + rating_relationship: 'Establishing a trusting relationship with the client', + rating_listening: 'Deep, active listening', + rating_questions: 'Using "strong" questions', + rating_communication: 'Direct communication', + rating_awareness: 'Developing and stimulating awareness', + rating_progress: 'Progress and Responsibility Management' }, agreementText: 'I have read and agree with the terms of the User Agreement,', userAgreement: 'User Agreement', @@ -109,10 +144,10 @@ export default { seminars: 'Seminars', courses: 'Courses', mba: 'MBA Information', - aboutCoach: 'About Coach', + aboutCoach: 'About Expert', skillsInfo: 'Skills Info', education: 'Education', - coaching: 'Coaching', + coaching: 'Expert profile', experiences: 'Practical experience', payInfo: 'Payment Info', sessionDuration: 'Session duration', @@ -146,6 +181,10 @@ export default { saturday: 'Sa', addNew: 'Add New', mExperiences: 'Managerial Experience', + pay: 'Pay', + sessionWishes: 'Write your wishes about the session', + successPayment: 'Successful Payment', + errorPayment: 'Payment Error', errors: { invalidEmail: 'The email address is not valid', emptyEmail: 'Please enter your E-mail', diff --git a/src/i18nKeys/es.ts b/src/i18nKeys/es.ts index f869ff2..a68ff8b 100644 --- a/src/i18nKeys/es.ts +++ b/src/i18nKeys/es.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Próximas y recientes sesiones', + rooms: 'Habitaciones', notifications: 'Notificación', support: 'Ayuda y asistencia', information: 'Información jurídica', @@ -42,13 +43,47 @@ export default { addComment: 'Añadir nuevo comentario', commentPlaceholder: 'Tu comentario', clientComments: 'Comentarios del cliente', - coachComments: 'Comentarios del entrenador' + coachComments: 'Comentarios del experto' }, room: { upcoming: 'Próximas salas', requested: 'Salas solicitadas', recent: 'Salas recientes', - newRoom: 'Nueva sala' + newRoom: 'Nueva sala', + editRoom: 'Editar la sala', + date: 'Fecha', + time: 'Tiempo', + maxParticipants: 'Máximo de participantes permitidos', + presenceOfSupervisor: 'Presencia de un supervisor', + supervisor: 'Supervisor', + members: 'Miembros', + participants: 'Participantes', + roomCreator: 'Creador de salas', + inviteSupervisor: 'Invitar al supervisor', + joinSupervisor: 'Unirse como supervisor', + inviteParticipant: 'Invitar a un participante', + joinParticipant: 'Unirse como participante', + rapport: 'Buena relación', + invite: 'Invitar', + save: 'Guardar sala', + rate: 'Valorar', + tellAboutReason: 'Cuéntanos qué ha pasado', + rating_raport: 'Buena relación', + rating_position_and_presence: 'Puesto de coach o presencia de coach', + rating_balance_and_frustration: 'Equilibrio entre apoyo y frustración', + rating_agreement: 'Crear un acuerdo de coaching (contrato de sesión)', + rating_planning_and_goals: 'Planear y establecer los objetivos', + rating_reality: 'Clarificar la realidad', + rating_opportunities: 'Nuevas oportunidades encontradas', + rating_action_plan: 'Se ha diseñado un plan de acción', + rating_motivation: 'Fuentes de motivación encontradas', + rating_next_session_stretch: 'Queda un poco para la siguiente sesión', + rating_relationship: 'Establecer una relación de confianza con el cliente', + rating_listening: 'Escucha activa y profunda', + rating_questions: 'Usar preguntas "contundentes"', + rating_communication: 'Comunicación directa', + rating_awareness: 'Desarrollar y estimular la conciencia', + rating_progress: 'Progreso y gestión de la responsabilidad' }, agreementText: 'He leído y acepto las condiciones del Acuerdo de usuario,', userAgreement: 'Acuerdo de usuario', @@ -110,9 +145,9 @@ export default { seminars: 'Seminarios', courses: 'Cursos', mba: 'Información sobre máster en ADE (MBA)', - aboutCoach: 'Sobre el coach', + aboutCoach: 'Acerca del experto', education: 'Educación', - coaching: 'Coaching', + coaching: 'Perfil del experto', experiences: 'Experiencia práctica', payInfo: 'Información de pago', sessionDuration: 'Duración de la sesión', @@ -146,6 +181,10 @@ export default { saturday: 'S', addNew: 'Añadir nuevo', mExperiences: 'Experiencia de dirección', + pay: 'Pago', + sessionWishes: 'Escribe tus deseos sobre la sesión', + successPayment: 'Pago Exitoso', + errorPayment: 'Error de Pago', errors: { invalidEmail: 'La dirección de correo electrónico no es válida', emptyEmail: 'Introduce tu correo electrónico', diff --git a/src/i18nKeys/fr.ts b/src/i18nKeys/fr.ts index 6bad2ed..5d49044 100644 --- a/src/i18nKeys/fr.ts +++ b/src/i18nKeys/fr.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Sessions futures et récentes', + rooms: 'Chambres', notifications: 'Notification', support: 'Aide et support', information: 'Informations légales', @@ -42,13 +43,47 @@ export default { addComment: 'Ajouter un nouveau commentaire', commentPlaceholder: 'Votre commentaire', clientComments: 'Commentaires du client', - coachComments: 'Commentaires du coach' + coachComments: 'Commentaires de l\'expert' }, room: { upcoming: 'Salles futures', requested: 'Salles demandées', recent: 'Salles récentes', - newRoom: 'Nouvelle salle' + newRoom: 'Nouvelle salle', + editRoom: 'Modifier la salle', + date: 'Date', + time: 'Temps', + maxParticipants: 'Max de participants autorisés', + presenceOfSupervisor: 'Présence d\'un superviseur', + supervisor: 'Superviseur', + members: 'Membres', + participants: 'Participants', + roomCreator: 'Créateur de la salle', + inviteSupervisor: 'Inviter un superviseur', + joinSupervisor: 'Rejoindre en tant que superviseur', + inviteParticipant: 'Inviter un participant', + joinParticipant: 'Rejoindre en tant que participant', + rapport: 'Rapport', + invite: 'Inviter', + save: 'Sauvegarder la salle', + rate: 'Noter', + tellAboutReason: 'Dites-nous ce qui s\'est passé', + rating_raport: 'Rapport', + rating_position_and_presence: 'Poste de coach ou présence de coach', + rating_balance_and_frustration: 'Équilibre entre assistance et frustration', + rating_agreement: 'Création d\'un contrat de coaching (contrat de séance)', + rating_planning_and_goals: 'Planification et définition des objectifs', + rating_reality: 'Clarification de la réalité', + rating_opportunities: 'Nouvelles opportunités trouvées', + rating_action_plan: 'Un plan d\'action a été établi', + rating_motivation: 'Sources de motivation trouvées', + rating_next_session_stretch: 'Une période est présente pour la prochaine session', + rating_relationship: 'Établissement d\'une relation de confiance avec le client', + rating_listening: 'Écoute approfondie et active', + rating_questions: 'Utilisation de questions «fortes»', + rating_communication: 'Communication directe', + rating_awareness: 'Développement et stimulation de la prise de conscience', + rating_progress: 'Gestion de la progression et de la responsabilité' }, agreementText: 'J\'ai lu et j\'accepte les dispositions de l\'Accord Utilisateur et de la', userAgreement: '', @@ -110,9 +145,9 @@ export default { seminars: 'Séminaires', courses: 'Cours', mba: 'Infos Maîtrise en gestion', - aboutCoach: 'À propos du coach', + aboutCoach: 'À propos de l\'expert', education: 'Éducation', - coaching: 'Coaching', + coaching: 'Profil de l\'expert', experiences: 'Expérience pratique', payInfo: 'Infos sur le paiement', sessionDuration: 'Durée de la session', @@ -146,6 +181,10 @@ export default { saturday: 'Sa', addNew: 'Ajouter un nouveau', mExperiences: 'Expérience en gestion', + pay: 'Paiement', + sessionWishes: 'Écrivez vos souhaits concernant la session', + successPayment: 'Paiement Réussi', + errorPayment: 'Erreur de Paiement', errors: { invalidEmail: 'L\'adresse e-mail n\'est pas valide', emptyEmail: 'Veuillez saisir votre e-mail', diff --git a/src/i18nKeys/it.ts b/src/i18nKeys/it.ts index 81a7d39..a6b5db8 100644 --- a/src/i18nKeys/it.ts +++ b/src/i18nKeys/it.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Prossime e recenti sessioni', + rooms: 'Stanze', notifications: 'Notifica', support: 'Assistenza e supporto', information: 'Informazioni legali', @@ -42,13 +43,47 @@ export default { addComment: 'Aggiungi nuovo commento', commentPlaceholder: 'Il tuo commento', clientComments: 'Commenti del cliente', - coachComments: 'Commenti dell\'allenatore' + coachComments: 'Commenti dell\'esperto' }, room: { upcoming: 'Prossime sale', requested: 'Sale richieste', recent: 'Sale recenti', - newRoom: 'Nuova sala' + newRoom: 'Nuova sala', + editRoom: 'Modifica sala', + date: 'Data', + time: 'Tempo', + maxParticipants: 'Numero massimo di partecipanti consentiti', + presenceOfSupervisor: 'Presenza di un relatore', + supervisor: 'Relatore', + members: 'Iscritti', + participants: 'Partecipanti', + roomCreator: 'Creatore sala', + inviteSupervisor: 'Invita relatore', + joinSupervisor: 'Partecipa come relatore', + inviteParticipant: 'Invita partecipante', + joinParticipant: 'Partecipa come partecipante', + rapport: 'Rapporto', + invite: 'Invita', + save: 'Salva sala', + rate: 'Valuta', + tellAboutReason: 'Descrivi cosa è successo', + rating_raport: 'Rapporto', + rating_position_and_presence: 'Posizione di coaching o presenza di coaching', + rating_balance_and_frustration: 'Equilibrio tra sostegno e frustrazione', + rating_agreement: 'Creazione di un accordo di coaching (contratto di sessione)', + rating_planning_and_goals: 'Pianificazione e definizione di obiettivi', + rating_reality: 'Chiarimento della realtà', + rating_opportunities: 'Nuove opportunità trovate', + rating_action_plan: 'È stato elaborato un piano d\'azione', + rating_motivation: 'Fonti di motivazione trovate', + rating_next_session_stretch: 'Esiste un\'estensione per la prossima sessione', + rating_relationship: 'Instaurazione di un rapporto di fiducia con il cliente', + rating_listening: 'Ascolto profondo e attivo', + rating_questions: 'Utilizzo di domande "forti"', + rating_communication: 'Comunicazione diretta', + rating_awareness: 'Sviluppo e stimolo della consapevolezza', + rating_progress: 'Gestione dei progressi e delle responsabilità' }, agreementText: 'Ho letto e accetto i termini dell\'Accordo con l\'utente,', userAgreement: '', @@ -110,9 +145,9 @@ export default { seminars: 'Seminari', courses: 'Corsi', mba: 'Info sull\'MBA', - aboutCoach: 'Informazioni sul coach', + aboutCoach: 'Informazioni sull\'esperto', education: 'Istruzione', - coaching: 'Coaching', + coaching: 'Profilo dell\'esperto', experiences: 'Esperienza pratica', payInfo: 'Info pagamento', sessionDuration: 'Durata della sessione', @@ -146,6 +181,10 @@ export default { saturday: 'Sa', addNew: 'Aggiungi nuovo', mExperiences: 'Esperienza manageriale', + pay: 'Pagamento', + sessionWishes: 'Scrivi i tuoi desideri riguardo alla sessione', + successPayment: 'Pagamento Riuscito', + errorPayment: 'Errore di Pagamento', errors: { invalidEmail: 'L\'indirizzo e-mail non è valido', emptyEmail: 'Inserisci l\'e-mail', diff --git a/src/i18nKeys/ru.ts b/src/i18nKeys/ru.ts index 84f82b6..e313458 100644 --- a/src/i18nKeys/ru.ts +++ b/src/i18nKeys/ru.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Предстоящие и недавние сессии', + rooms: 'Комнаты', notifications: 'Уведомления', support: 'Служба поддержки', information: 'Юридическая информация', @@ -42,13 +43,47 @@ export default { addComment: 'Добавить новый', commentPlaceholder: 'Ваш комментарий', clientComments: 'Комментарии клиента', - coachComments: 'Комментарии коуча' + coachComments: 'Комментарии эксперта' }, room: { upcoming: 'Предстоящие комнаты', requested: 'Запрошенные комнаты', recent: 'Недавние комнаты', - newRoom: 'Новая комната' + newRoom: 'Новая комната', + editRoom: 'Изменить комнату', + date: 'Дата', + time: 'Время', + maxParticipants: 'Макс. кол-во участников', + presenceOfSupervisor: 'Присутствие супервизора', + supervisor: 'Супервайзер', + members: 'Участники', + participants: 'Участники', + roomCreator: 'Создатель комнаты', + inviteSupervisor: 'Пригласить супервизора', + joinSupervisor: 'Присоединиться как супервизор', + inviteParticipant: 'Пригласить участника', + joinParticipant: 'Присоединиться как участник', + rapport: 'Раппорт', + invite: 'Пригласить', + save: 'Сохранить комнату', + rate: 'Оценить', + tellAboutReason: 'Расскажите, что произошло', + rating_raport: 'Раппорт', + rating_position_and_presence: 'Коуч-позиция или коучинговое присутствие', + rating_balance_and_frustration: 'Баланс поддержки и фрустрации', + rating_agreement: 'Создание коучингового соглашения (контракт на сессию)', + rating_planning_and_goals: 'Планирование и постановка целей', + rating_reality: 'Прояснение реальности', + rating_opportunities: 'Найдены новые возможности', + rating_action_plan: 'Составлен план действий', + rating_motivation: 'Найдены источники мотивации', + rating_next_session_stretch: 'Есть "протяжка" на следующую сессию', + rating_relationship: 'Установление доверительных отношений с клиентом', + rating_listening: 'Глубокое активное слушание', + rating_questions: 'Использование сильных вопросов', + rating_communication: 'Прямая коммуникация', + rating_awareness: 'Развитие и стимулирование осознанности', + rating_progress: 'Управление прогрессом и ответственностью' }, agreementText: 'Я прочитал и согласен с условиями Пользовательского соглашения,', userAgreement: 'Пользовательского соглашения', @@ -111,9 +146,9 @@ export default { courses: 'Курсы', mba: 'Информация о MBA', experiences: 'Практический опыт', - aboutCoach: 'О коуче', + aboutCoach: 'Информация об эксперте', education: 'Образование', - coaching: 'Коучинг', + coaching: 'Профиль эксперта', payInfo: 'Платежная информация', sessionDuration: 'Продолжительность сессии', experienceHours: 'Общее количество часов практического опыта', @@ -146,6 +181,10 @@ export default { saturday: 'Сб', addNew: 'Добавить', mExperiences: 'Управленческий опыт', + pay: 'Оплата', + sessionWishes: 'Напишите свои пожелания по поводу сессии', + successPayment: 'Успешная оплата', + errorPayment: 'Ошибка оплаты', chat: { join: 'начать чат', joinAI: 'начать чат с ИИ' diff --git a/src/lib/stripe.ts b/src/lib/stripe.ts new file mode 100644 index 0000000..08773a7 --- /dev/null +++ b/src/lib/stripe.ts @@ -0,0 +1,11 @@ +import "server-only"; + +import Stripe from "stripe"; + +export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { + apiVersion: "2024-06-20", + appInfo: { + name: "bbuddy-ui", + url: "", + }, +}); \ No newline at end of file diff --git a/src/styles/_default.scss b/src/styles/_default.scss index 93f42f9..c871c19 100644 --- a/src/styles/_default.scss +++ b/src/styles/_default.scss @@ -668,6 +668,7 @@ a { & > div { display: flex; gap: 4px; + padding-left: 1px; &:first-child { flex-direction: column; diff --git a/src/styles/_modal.scss b/src/styles/_modal.scss index 05be32d..0b9c91a 100644 --- a/src/styles/_modal.scss +++ b/src/styles/_modal.scss @@ -26,7 +26,7 @@ } } - &__comment__content { + &__comment__content, &__report__content { display: flex; flex-direction: column; padding: 44px 40px; @@ -82,6 +82,13 @@ } } } + + &__users-list__content { + display: flex; + flex-direction: column; + padding: 40px; + gap: 24px; + } } .ant-modal-mask { diff --git a/src/styles/_pages.scss b/src/styles/_pages.scss index eb8eafc..4579515 100644 --- a/src/styles/_pages.scss +++ b/src/styles/_pages.scss @@ -931,6 +931,10 @@ &.chosen { color: #D93E5C; } + + &.history { + color: #c4c4c4; + } } } } diff --git a/src/styles/sessions/_agora.scss b/src/styles/sessions/_agora.scss index 4e49e64..e4baf4f 100644 --- a/src/styles/sessions/_agora.scss +++ b/src/styles/sessions/_agora.scss @@ -2,9 +2,12 @@ &__wrap { width: 100%; height: 716px; - border-radius: 16px; position: relative; overflow: hidden; + + &__single { + border-radius: 16px; + } } &__container { @@ -25,6 +28,16 @@ justify-content: space-between; align-items: flex-end; z-index: 2; + + &_group { + width: 100%; + display: flex; + justify-content: center; + background: rgba(0, 59, 70, 0.4); + padding: 16px; + border-radius: 16px; + margin-top: 24px; + } } &__controls { @@ -126,6 +139,48 @@ position: absolute; display: flex; } + + &_groups { + width: 100%; + height: 100%; + display: flex; + flex-wrap: wrap; + gap: 16px; + justify-content: center; + + & > div { + border-radius: 16px; + overflow: hidden; + + video { + object-fit: contain !important; + } + } + + &.gr-1 { + & > div { + width: 100%; + } + } + + &.gr-2, &.gr-3, &.gr-4 { + & > div { + flex: calc((100% - 16px) / 2) 0; + } + } + + &.gr-5, &.gr-6, &.gr-7, &.gr-8, &.gr-9 { + & > div { + flex: calc((100% - 16px * 2) / 3) 0; + } + } + + &.gr-10, &.gr-11, &.gr-12, &.gr-13, &.gr-14, &.gr-15, &.gr-16 { + & > div { + flex: calc((100% - 16px * 3) / 4) 0; + } + } + } } &__video { diff --git a/src/styles/sessions/_details.scss b/src/styles/sessions/_details.scss index e8430da..dcbc525 100644 --- a/src/styles/sessions/_details.scss +++ b/src/styles/sessions/_details.scss @@ -18,6 +18,11 @@ background: lightgray 50%; box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32); overflow: hidden; + + &_small { + width: 86px; + height: 86px; + } } &__inner { @@ -41,6 +46,41 @@ line-height: 120%; } + &__supervisor-comment { + width: 100%; + background: #E4F5FA; + padding: 8px; + border-radius: 0 8px 8px 8px; + color: #66A5AD; + @include rem(13); + font-weight: 500; + line-height: 120%; + } + + &__report-list { + display: flex; + width: 100%; + flex-direction: column; + gap: 8px; + + & > div { + width: 100%; + color: #4E7C86; + @include rem(13); + font-weight: 500; + line-height: 120%; + display: flex; + gap: 8px; + justify-content: space-between; + align-items: flex-end; + } + + &_divider { + flex: 1; + border-bottom: 1px solid #E4F5FA; + } + } + &__comments { display: flex; flex-direction: column; @@ -200,6 +240,31 @@ } } + &__filled { + user-select: none; + outline: none !important; + border: none !important; + text-decoration: none; + cursor: pointer; + border-radius: 8px !important; + background: #66A5AD !important; + box-shadow: none !important; + display: flex; + height: 54px !important; + padding: 15px 24px; + justify-content: center; + align-items: center; + color: #fff !important; + @include rem(15); + font-style: normal; + font-weight: 400; + line-height: 160%; + + &:hover, &:active { + color: #fff !important; + } + } + &__header { display: flex; padding-bottom: 8px; @@ -268,6 +333,54 @@ overflow: hidden; } + &__profile { + display: flex; + flex-direction: column; + gap: 16px; + padding-top: 16px; + align-items: flex-start; + border-top: 1px solid #C4DFE6; + + &_title { + width: 100%; + gap: 16px; + display: flex; + justify-content: space-between; + + div { + @include rem(18); + font-weight: 600; + line-height: 150%; + color: #6FB98F; + + &:first-child { + color: #003B46; + } + } + } + + &_list { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + } + + &_item { + display: flex; + gap: 16px; + justify-content: space-between; + + .card-detail__inner { + justify-content: center; + } + + .card-detail__name { + color: #2C7873; + } + } + } + &__footer { display: flex; justify-content: flex-end; diff --git a/src/styles/view/_calendar.scss b/src/styles/view/_calendar.scss new file mode 100644 index 0000000..3431bf4 --- /dev/null +++ b/src/styles/view/_calendar.scss @@ -0,0 +1,61 @@ +.b-calendar { + padding: 44px 40px !important; + + &-month { + text-transform: capitalize; + } + + &-header { + justify-content: center; + border-bottom: none !important; + } + + &-cell { + span { + color: #66A5AD; + } + + &__weekend { + span { + color: #FFBD00; + } + } + } + + .ant-picker-body { + margin-bottom: -42px !important; + } + + .ant-picker-panel { + border-top: none !important; + margin-top: 12px; + } + + .ant-picker-cell { + opacity: 0 !important; + + &-disabled { + &::before { + background: transparent !important; + } + + span { + color: rgba(0, 0, 0, 0.25) !important; + } + } + + &.ant-picker-cell-in-view { + opacity: 1 !important; + background: transparent !important; + } + } + + th, td { + vertical-align: middle !important; + height: 40px !important; + } + + th { + color: #66A5AD !important; + } +} \ No newline at end of file diff --git a/src/styles/view/_datepicker.scss b/src/styles/view/_datepicker.scss new file mode 100644 index 0000000..ede86fb --- /dev/null +++ b/src/styles/view/_datepicker.scss @@ -0,0 +1,128 @@ +.b-datepicker { + width: 100% !important; + height: 54px !important; + + &.ant-picker-filled { + background: transparent !important; + z-index: 1; + padding-top: 22px !important; + padding-left: 16px !important; + + &:hover { + border-color: #2c7873 !important; + } + + .ant-picker-input { + input { + font-size: 14px !important; + } + } + } + + .ant-picker-suffix { + margin-top: -20px; + } + + &-wrap { + position: relative; + width: 100%; + background-color: #F8F8F7; + border-radius: 8px; + + &.b-datepicker__active .b-datepicker-label { + font-size: 12px; + font-weight: 300; + line-height: 14px; + top: 8px; + } + } + + &-label { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; + color: #000; + opacity: .3; + position: absolute; + left: 16px; + top: 15px; + right: 22px; + z-index: 0; + transition: all .1s ease; + overflow: hidden; + text-overflow: ellipsis; + + span { + white-space: nowrap; + } + } + + &-popup { + padding: 16px !important; + + .ant-picker-date-panel { + padding: 16px 8px !important; + } + + .ant-picker-header-view { + color: #2c7873 !important; + } + + .ant-picker-header { + border: none !important; + + .ant-picker-header-super-prev-btn, .ant-picker-header-super-next-btn { + display: none !important; + } + } + + .ant-picker-cell { + opacity: 0 !important; + padding: 0 !important; + + &:not(.ant-picker-cell-disabled) { + color: #66A5AD !important; + + &:hover { + .ant-picker-cell-inner { + color: #6FB98F !important; + background: transparent !important; + } + } + } + + &-selected:not(.ant-picker-cell-disabled) .ant-picker-cell-inner { + color: #6FB98F !important; + background: transparent !important; + } + + &-disabled { + color: rgba(0, 0, 0, 0.25) !important; + + &::before { + background: transparent !important; + } + } + + &.ant-picker-cell-in-view { + opacity: 1 !important; + background: transparent !important; + } + + } + + .ant-picker-cell-inner::before { + border: none !important; + } + + th, td { + vertical-align: middle !important; + height: 36px !important; + } + + th { + color: #66A5AD !important; + } + } +} diff --git a/src/styles/view/_radio.scss b/src/styles/view/_radio.scss new file mode 100644 index 0000000..8e3bf9b --- /dev/null +++ b/src/styles/view/_radio.scss @@ -0,0 +1,3 @@ +.ant-form-item-has-error .ant-radio-inner { + border-color: #ff4d4f !important; +} \ No newline at end of file diff --git a/src/styles/view/_rate.scss b/src/styles/view/_rate.scss index e194d35..f312b09 100644 --- a/src/styles/view/_rate.scss +++ b/src/styles/view/_rate.scss @@ -14,4 +14,25 @@ color: #c4dfe6 !important; } } + + &-list { + display: flex; + flex-direction: column; + gap: 24px; + + &__item { + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; + + &_title { + color: #2C7873; + @include rem(13); + font-weight: 500; + line-height: 120%; + text-align: center; + } + } + } } diff --git a/src/styles/view/_room.scss b/src/styles/view/_room.scss new file mode 100644 index 0000000..71c7087 --- /dev/null +++ b/src/styles/view/_room.scss @@ -0,0 +1,86 @@ +.card-room { + &__details { + width: 100%; + display: grid; + grid-template-columns: 120px auto; + gap: 4px 8px; + + div { + @include rem(13); + font-weight: 500; + line-height: 120%; + color: #2C7873; + + &:nth-child(2n) { + color: #6FB98F; + } + } + } +} + +.b-users-list { + width: 100%; + display: flex; + flex-direction: column; + gap: 24px; + padding: 0 16px; + + &__empty { + color: gray; + } + + &-item { + padding: 0 0 16px; + border-bottom: 1px solid #C4DFE6; + display: flex; + flex-direction: column; + gap: 16px; + + &:last-child { + border-bottom: none; + padding: 0; + } + + & > div { + display: flex; + gap: 16px; + align-items: center; + } + } +} + +.b-room-form { + &__grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + align-items: center; + } + + .ant-form-item { + margin-bottom: 0 !important; + } + + .card-detail__apply { + align-self: flex-start; + } + + .b-room-switch { + label { + margin-right: 24px; + &:after { + display: none !important; + } + } + + & > div { + justify-content: space-between; + } + } +} + +.ant-select-item-option-content { + span { + text-transform: capitalize; + } +} \ No newline at end of file diff --git a/src/styles/view/_schedule.scss b/src/styles/view/_schedule.scss new file mode 100644 index 0000000..49cab71 --- /dev/null +++ b/src/styles/view/_schedule.scss @@ -0,0 +1,31 @@ +.b-schedule { + &-time { + padding: 44px 40px; + display: flex; + flex-direction: column; + gap: 24px; + + .b-button-link-big { + font-size: 24px; + line-height: 32px; + color: #6FB98F !important; + font-family: var(--font-comfortaa); + padding: 0 !important; + border: none !important; + text-transform: capitalize; + } + } + + &-radio-list { + .ant-radio-group { + display: flex; + flex-direction: column; + gap: 12px; + } + } + + &-payment { + padding: 44px 40px; + min-height: 300px; + } +} \ No newline at end of file diff --git a/src/styles/view/_select.scss b/src/styles/view/_select.scss index a68558e..fe853b6 100644 --- a/src/styles/view/_select.scss +++ b/src/styles/view/_select.scss @@ -3,11 +3,12 @@ height: 54px !important; .ant-select-selector { - background-color: #F8F8F7 !important; + background-color: transparent !important; border-color: #F8F8F7 !important; border-radius: 8px !important; padding: 22px 16px 8px !important; box-shadow: none !important; + z-index: 1; .ant-select-selection-item { font-size: 15px !important; @@ -17,6 +18,12 @@ } } + &.ant-select-status-error { + .ant-select-selector { + border-color: #ff4d4f !important; + } + } + .ant-select-selection-overflow-item { margin-right: 4px; } @@ -35,6 +42,9 @@ &-wrap { position: relative; width: 100%; + background-color: #F8F8F7; + border-radius: 8px; + &.b-multiselect__active .b-multiselect-label { font-size: 12px; font-weight: 300; @@ -44,12 +54,12 @@ } &-label { - font-size: 15px; + font-size: 14px; font-style: normal; font-weight: 400; line-height: 24px; color: #000; - opacity: .3; + opacity: .4; position: absolute; left: 16px; top: 15px; @@ -70,11 +80,12 @@ height: 54px !important; .ant-select-selector { - background-color: #F8F8F7 !important; + background-color: transparent !important; border-color: #F8F8F7 !important; border-radius: 8px !important; padding: 22px 16px 8px !important; box-shadow: none !important; + z-index: 1; .ant-select-selection-item { font-size: 15px !important; @@ -84,6 +95,12 @@ } } + &.ant-select-status-error { + .ant-select-selector { + border-color: #ff4d4f !important; + } + } + .ant-select-arrow { color: #2c7873 !important; } @@ -98,6 +115,8 @@ &-wrap { position: relative; width: 100%; + background-color: #F8F8F7; + border-radius: 8px; &.b-select__active .b-select-label { font-size: 12px; @@ -113,7 +132,7 @@ font-weight: 400; line-height: 24px; color: #000; - opacity: .3; + opacity: .4; position: absolute; left: 16px; top: 15px; diff --git a/src/styles/view/_timepicker.scss b/src/styles/view/_timepicker.scss index 6d5cda3..e98e282 100644 --- a/src/styles/view/_timepicker.scss +++ b/src/styles/view/_timepicker.scss @@ -13,7 +13,7 @@ .ant-picker-input { input { - font-size: 15px !important; + font-size: 14px !important; } } } diff --git a/src/styles/view/style.scss b/src/styles/view/style.scss index b626bbd..4eb5a4d 100644 --- a/src/styles/view/style.scss +++ b/src/styles/view/style.scss @@ -9,3 +9,8 @@ @import "_practice.scss"; @import "_collapse.scss"; @import "_timepicker.scss"; +@import "_datepicker.scss"; +@import "_calendar.scss"; +@import "_schedule.scss"; +@import "_radio.scss"; +@import "_room.scss"; diff --git a/src/types/author.ts b/src/types/author.ts index f836cc4..b36d83e 100644 --- a/src/types/author.ts +++ b/src/types/author.ts @@ -1,5 +1,4 @@ import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful' -import {BlogPostFields} from "./blogPost"; import {ContentImage} from "../lib/contentful/contentImage"; export interface AuthorFields { diff --git a/src/types/experts.ts b/src/types/experts.ts index 0e98e83..5b16577 100644 --- a/src/types/experts.ts +++ b/src/types/experts.ts @@ -71,3 +71,24 @@ export type ExpertDetails = { associations?: Association[]; associationLevels?: AssociationLevel[]; }; + +export type Slot = { + startTime: string; + endTime: string; +} + +export type ExpertScheduler = { + tags: Tag[], + availableSlots: Slot[]; +} + +export type ExpertSchedulerSession = { + sessionId: string +}; + +export type SignupSessionData = { + coachId: number, + tagId?: number, + startAtUtc?: string, + clientComment?: string +}; \ No newline at end of file diff --git a/src/types/payment.ts b/src/types/payment.ts new file mode 100644 index 0000000..cdadd42 --- /dev/null +++ b/src/types/payment.ts @@ -0,0 +1,3 @@ +export type Payment = { + amount: number; +} \ No newline at end of file diff --git a/src/types/rooms.ts b/src/types/rooms.ts new file mode 100644 index 0000000..c191e3d --- /dev/null +++ b/src/types/rooms.ts @@ -0,0 +1,57 @@ +import { PublicUser, Session, SessionState } from './sessions'; +import { Tag } from './tags'; +import { Slot } from './experts'; + +export enum RoomsType { + UPCOMING = 'upcoming', + RECENT = 'recent', + NEW = 'new', +} + +export type Record = { + id: number; + sessionId: number; + sid?: string; + resourceId?: string; + readyForLoad?: boolean; + cname?: string; +} + +export type Room = Session & { recordings?: Record[] }; + +export type GetUsersForRooms = { + items?: PublicUser[], + isTooManyResults?: boolean; +} + +export type RoomEdit = { + id: number, + scheduledStartAtUtc?: string, + scheduledEndAtUtc?: string, + state?: SessionState, + cost?: number, + maxClients?: number, + title?: string, + description?: string, + isNeedSupervisor?: boolean, + tagIds?: number[] +}; + +export type RoomEditDTO = { + item: RoomEdit; + tags?: Tag[]; + availableSlots: Slot[]; +}; + +export type Report = { + evaluationCriteriaId: number, + evaluationCriteriaName?: string, + score?: number, + key?: string +}; + +export type ReportData = { + sessionId: number, + sessionSupervisorScores: Report[], + supervisorComment?: string +}; diff --git a/src/types/sessions.ts b/src/types/sessions.ts index 2c271ab..6d3a7d7 100644 --- a/src/types/sessions.ts +++ b/src/types/sessions.ts @@ -6,6 +6,8 @@ export type PublicUser = { name?: string; surname?: string; faceImageUrl?: string; + coachBotId?: number; + parentId?: number; }; // type User = { @@ -148,6 +150,7 @@ export type Session = { themesTags?: SessionTag[]; coachComments?: SessionComment[]; clientComments?: SessionComment[]; + creatorId?: number; }; export enum SessionType { diff --git a/src/utils/account.ts b/src/utils/account.ts index 88aeb9d..cf8eb0c 100644 --- a/src/utils/account.ts +++ b/src/utils/account.ts @@ -2,7 +2,7 @@ import { message } from 'antd'; import type { UploadFile } from 'antd'; import { i18nText } from '../i18nKeys'; -const ROUTES = ['sessions', 'notifications', 'support', 'information', 'settings', 'messages', 'expert-profile']; +const ROUTES = ['sessions', 'rooms', 'notifications', 'support', 'information', 'settings', 'messages', 'expert-profile']; const COUNTS: Record = { sessions: 12, notifications: 5 diff --git a/src/utils/get-stripe.ts b/src/utils/get-stripe.ts new file mode 100644 index 0000000..e4cf2dd --- /dev/null +++ b/src/utils/get-stripe.ts @@ -0,0 +1,12 @@ +import { Stripe, loadStripe } from '@stripe/stripe-js'; + +let stripePromise: Promise; + +export default function getStripe(): Promise { + if (!stripePromise) + stripePromise = loadStripe( + process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string, + ); + + return stripePromise; +} \ No newline at end of file diff --git a/src/utils/locale.ts b/src/utils/locale.ts new file mode 100644 index 0000000..596a35c --- /dev/null +++ b/src/utils/locale.ts @@ -0,0 +1,28 @@ +import locale_ru from 'antd/lib/calendar/locale/ru_RU'; +import locale_en from 'antd/lib/calendar/locale/en_GB'; +import locale_de from 'antd/lib/calendar/locale/de_DE'; +import locale_it from 'antd/lib/calendar/locale/it_IT'; +import locale_es from 'antd/lib/calendar/locale/es_ES'; +import locale_fr from 'antd/lib/calendar/locale/fr_FR'; + +// for calendars +export const getLocale = (locale: string) => { + if (locale) { + switch (locale) { + case 'ru': + return locale_ru; + case 'de': + return locale_de; + case 'fr': + return locale_fr; + case 'it': + return locale_it; + case 'es': + return locale_es; + default: + return locale_en; + } + } + + return locale_en; +}; \ No newline at end of file diff --git a/src/utils/stripe-helpers.ts b/src/utils/stripe-helpers.ts new file mode 100644 index 0000000..09144bd --- /dev/null +++ b/src/utils/stripe-helpers.ts @@ -0,0 +1,30 @@ +export function formatAmountForDisplay( + amount: number, + currency: string, +): string { + let numberFormat = new Intl.NumberFormat(["en-US"], { + style: "currency", + currency: currency, + currencyDisplay: "symbol", + }); + return numberFormat.format(amount); +} + +export function formatAmountForStripe( + amount: number, + currency: string, +): number { + let numberFormat = new Intl.NumberFormat(["en-US"], { + style: "currency", + currency: currency, + currencyDisplay: "symbol", + }); + const parts = numberFormat.formatToParts(amount); + let zeroDecimalCurrency: boolean = true; + for (let part of parts) { + if (part.type === "decimal") { + zeroDecimalCurrency = false; + } + } + return zeroDecimalCurrency ? amount : Math.round(amount * 100); +} \ No newline at end of file