diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4d29575de --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/README.md b/README.md index 03779cb84..43a38965d 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# rslang \ No newline at end of file +# rslang + +Front-end Таска "Rslang" команда #108 + +для локального развёртывания приложения необходимо выполнить следующие шаги: + +- клонировать репозиторий в локальную папку на компьютере, желательно не использовать в пути к папке приложения русских букв и большой вложенности папок. + +- установить необходимые зависимости приложения командой npm install + +- после успешной установки можно запустить приложение командой react-scripts start, приложение скомпилируется и откроется на dev-сервере + +- в файле /src/api/defData в константе url устанавливается путь к api серверу базы данных, по умолчанию установлен https://rslang.tk + +- инструкция по развёртыванию api сервера в read.me бэкэнд сервера. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..b5c209139 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,345 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + }, + "@types/react": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz", + "integrity": "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-gauge-chart": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@types/react-gauge-chart/-/react-gauge-chart-0.3.0.tgz", + "integrity": "sha512-n5+3osoKXEXS8tdzEEB5cqRbfs/zo3tO0mMQFH5sDhZOQp1lnoh0pa+8Obl/E4ZBwOAUifGVOYRf5VOwFkdMaw==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-speech-recognition": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/react-speech-recognition/-/react-speech-recognition-3.6.0.tgz", + "integrity": "sha512-nFXlBxhz5Wnz/P7AQenmaG7r9MGBmvZhTW/MLPEPd1CYLgB+6jg0GygIYYM5LR+K1WyZ3+jXGhGszxoLQQkHXQ==" + }, + "@types/scheduler": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", + "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "csstype": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" + }, + "d3": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" + }, + "d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" + }, + "d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + }, + "d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + }, + "d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "react-gauge-chart": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/react-gauge-chart/-/react-gauge-chart-0.3.0.tgz", + "integrity": "sha512-W6oYFnlKNP5fuvERBwMuwjtQ+mNd+qpRjvjkTKrYeCmtBShCkxP17TOddtF5Sk5MbUJQle1MbER8TyJZaBLatA==", + "requires": { + "d3": "^5.12.0" + } + }, + "react-speech-recognition": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/react-speech-recognition/-/react-speech-recognition-3.7.0.tgz", + "integrity": "sha512-apJhZ94GCnW8/6qIbnWHZrZqgDfYyuvVzU1WdEOuDHLeIgQ5CSe3l7IwXbkvxXqYQp4/7DmYbOmvXt2rRahXyQ==", + "dev": true + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + } + } +} diff --git a/rslang/.gitignore b/rslang/.gitignore new file mode 100644 index 000000000..9d9101486 --- /dev/null +++ b/rslang/.gitignore @@ -0,0 +1,24 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +package-lock.json + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/rslang/README.md b/rslang/README.md new file mode 100644 index 000000000..dbf499138 --- /dev/null +++ b/rslang/README.md @@ -0,0 +1,13 @@ +Front-end Таска "Rslang" команда #108 + +для локального развёртывания приложения необходимо выполнить следующие шаги: + +- клонировать репозиторий в локальную папку на компьютере, желательно не использовать в пути к папке приложения русских букв и большой вложенности папок. + +- установить необходимые зависимости приложения командой npm install + +- после успешной установки можно запустить приложение командой react-scripts start, приложение скомпилируется и откроется на dev-сервере + +- в файле /src/api/defData в константе url устанавливается путь к api серверу базы данных, по умолчанию установлен https://rslang.tk + +- инструкция по развёртыванию api сервера в read.me бэкэнд сервера. \ No newline at end of file diff --git a/rslang/docs/api.md b/rslang/docs/api.md new file mode 100644 index 000000000..f801a24e5 --- /dev/null +++ b/rslang/docs/api.md @@ -0,0 +1,67 @@ + + baseUrl - http://rslang.tk:3000/ + baseUrl - https://rocky-basin-33827.herokuapp.com/ + baseUrl - https://serene-falls-78086.herokuapp.com/ + + /doc - документация api + +## getData +getWords - слова + + GET для получения списка слов: https://rocky-basin-33827.herokuapp.com/words?page=2&group=0 - получить слова со 2-й страницы группы 0 +Строка запроса должна содержать в себе номер группы и номер страницы. Всего 6 групп(от 0 до 5) и в каждой группе по 30 страниц(от 0 до 29). В каждой странице по 20 слов. Группы разбиты по сложности от самой простой(0) до самой сложной(5). + + +getUserData - получение даных пользователя + + getUserData(url, token) + + addUrl: + user - /users/{userId} + user words - /users/{userId}/words + user word - /users/{userId}/words/{wordId} + user settings - /users/{userId}/settings + user statistic - /users/{userId}/statistics + + \users\{id}\statistics и \users\{id}\settings +Объект optional у UserWord, Statistics, Settings имеет ограничение по размеру - не более 30 полей и общая длина объекта после JSON.stringify() не должна превышать 1500 символов. Структуру этих объектов вы разрабатываете сами исходя из требований и вашей реализации задачи. + + word - /words/{wordId} + words - /words?page=2&group=0 - получить слова со 2-й страницы группы 0 +Строка запроса должна содержать в себе номер группы и номер страницы. Всего 6 групп(от 0 до 5) и в каждой группе по 30 страниц(от 0 до 29). В каждой странице по 20 слов. Группы разбиты по сложности от самой простой(0) до самой сложной(5). + + +## setData +setUserData - сохранение даных пользователя в базу + + setUserData(url, token, body) + + addUrl: + user settings - /users/{userId}/settings + user statistic - /users/{userId}/statistics + +statistics и settings обнавляются по ключам, можно отправить в body объект с одним ключём для обновления одного внутреннего объекта, при этом объект по этому ключу полностью перезапишется, не изменяя всего остального содержания settings или statistics. + + statistics { + learnedWords: number, + optional: {}, + vocabulary: {}, + call: {}, + speakit: {}, + sprint: {}, + ourgame: {}, + puzzle: {}, + savanna: {} + } + + settings { + wordsPerDay: number, + optional: {}, + vocabulary: {}, + call: {}, + speakit: {}, + sprint: {}, + ourgame: {}, + puzzle: {}, + savanna: {} + } diff --git a/rslang/docs/task.md b/rslang/docs/task.md new file mode 100644 index 000000000..556646f16 --- /dev/null +++ b/rslang/docs/task.md @@ -0,0 +1,219 @@ +https://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-rslang.md + +RS Lang – приложение для изучения иностранных слов, включающее электронный учебник с базой слов для изучения, мини-игры для их повторения, страницу статистики для отслеживания индивидуального прогресса. + +электронная версия учебника "4000 Essential English Words" - из этого учебника необходимо воспроизвести Word List +приложение Lingualeo - из этого приложения необходимо воспроизвести мини-игры "Саванна", "Аудиовызов", "Спринт". +Для доступа к играм "Аудиовызов" и "Спринт" понадобится Lingualeo Premium. Бесплатный доступ к Lingualeo Premium на один день откроется после 5 дней тренировки. Также для знакомства с геймплеем можно использовать видео: Саванна и Аудиовызов, Спринт + +### Лучшие работы студентов предыдущего набора +(сейчас требования к заданию изменились) + +https://rslang-team16-arcanar7.web.app/ + +https://rslang-team41-jekman87.netlify.app/ + +https://rslang-team5-alekchaik.netlify.app/ + +https://rslang-team11-kagafon.netlify.app/ + +https://rslang-team69-dimonwhite.netlify.app/ + +https://rslang-team26-evgender.netlify.app/ + +https://rslang-team64-viktorsipach.netlify.app/ + +### Исходные данные +Коллекция "4000 essential english words". Коллекция содержит 3600 часто употребляемых английских слов, изучение которых вам необходимо организовать. Слова в коллекции отсортированы от более простых и известных к более сложным. Первые 400 наиболее часто употребляемых слов в коллекцию не вошли. Считается, что это базовый запас взрослого человека, оставшийся от предыдущих попыток изучения языка. Вся коллекция разбита на шесть групп, в каждой группе 30 страниц, на каждой странице 20 слов для изучения. + +## Структура приложения + +- главная страница приложения + +- электронный учебник со словарём + +- мини-игры "Саванна", "Аудиовызов", "Спринт", "Своя игра" + +- страница статистики + +### Описание функциональных блоков + +#### 1 Главная страница приложения +выполняет функцию промо-страницы, её оформление определяет первое впечатление о приложении +главная страница приложения содержит: +- меню с навигацией по учебнику, ссылками на мини-игры и статистику. Меню или иконка меню отображается на всех страницах приложения +- описание возможностей и преимуществ приложения +- небольшое (5-7 минут) видео с демонстрацией работы приложения +- раздел "О команде" с фото и ссылками на гитхабы всех участников команды, описанием вклада в разработку приложения каждого из них. При желании данный раздел можно вынести в отдельную страницу +- footer со ссылками на гитхабы авторов приложения, год создания приложения, логотип курса со ссылкой на курс. footer отображается на всех страницах приложения за исключением мини-игр. + + #### 2 Электронный учебник + - электронный учебник состоит из шести разделов, которым соответствуют шесть групп слов коллекции исходных данных. В каждом разделе 30 страниц. На каждой странице выводится: + - меню или иконка меню + - иконка настроек + - список из 20 слов + - ссылки на мини-игры "Саванна", "Аудиовызов", "Спринт", "Своя игра" для повторения изученных слов + - навигация по страницам со стрелками для перехода к следующей и предыдущей страницам и номером текущей страницы + - также необходимо продумать навигацию по шести разделам учебника и предусмотреть небольшие различия в оформлении каждого раздела. Например, можно использовать для каждого раздела индикатор определённого цвета + - при перезагрузке страницы открывается последняя открытая страница приложения + + ##### Настройки + - в настройках учебника у пользователя есть возможность указать: + - нужно ли отображать в списке слов перевод изучаемого слова и перевод предложений с ним + - нужно ли отображать возле каждого слова кнопки, при клике по которым данное слово добавляется в раздел словаря "Сложные слова" или "Удалённые слова" + + ##### Список слов + - для каждого слова отображается: + - само слово, его транскрипция, его перевод + - предложение с объяснением значения изучаемого слова, его перевод + - предложение с примером использования изучаемого слова, его перевод + - картинка-ассоциация к изучаемому слову + - иконка аудио при клике по которой последовательно звучит произношение изучаемого слова, произношение предложения с объяснением его значения, произношение предложения с примером его использования + - кнопки, при клике по которым изучаемое слово добавляется в разделы словаря "Сложные слова" или "Удалённые слова" + - результат изучения/повторения слова в мини-играх + - если слово добавлено в раздел словаря "Сложные слова", оно остаётся на странице учебника, и его стиль изменяется или возле него выводится индикатор, указывающий, что оно относится к сложным словам + - если слово добавлено в раздел словаря "Удалённые слова", оно удаляется со страницы учебника. Если пользователь удалит со страницы все слова, страница удаляется + + ##### Словарь + - словарь является частью учебника. В словаре есть разделы "Изучаемые слова", "Сложные слова", "Удалённые слова" + - в раздел "Изучаемые слова" попадают слова, которые были задействованы в мини-играх, если мини-игры открывались кликом по ссылке на странице учебника или на странице раздела словаря "Сложные слова". Также в раздел "Изучаемые слова" попадают слова, которые пользователь отметил как сложные. Возле сложных слов есть индикатор или они выделены стилем, так же, как и на странице учебника + - возле каждого слова в разделе "Изучаемые слова" указывается результат изучения - сколько раз слово было правильно угадано в мини-играх, сколько раз пользователь ошибался + - для каждого раздела и каждой страницы учебника указывается количество изучаемых слов и общий результат их изучения + - в разделы словаря "Сложные слова" и "Удалённые слова" слова попадают, если пользователь кликнул по соответствующим кнопкам возле слов на страницах учебника + - страницы разделов словаря "Сложные слова" и "Удалённые слова" выглядят точно так же, как страницы учебника: формируются страницы, на каждой из которых список из 20 слов, создаётся новая страница, на страницах есть ссылки на мини-игры для повторения слов. Слова из разных разделов учебника попадают на разные страницы, на странице есть индикатор, указывающий, к какому разделу учебника она относитеся. Если слов больше 20, создаётся новая страница. Единственное отличие в списке слов вместо кнопок, при клике по которым изучаемое слово добавляется в разделы словаря "Сложные слова" или "Удалённые слова", в словаре возле слова отображается кнопка "Восстановить", которая удаляет слово из словаря и восстанавливает его на странице электронного учебника + + #### Мини-игры "Саванна", "Аудиовызов", "Спринт", "Своя игра" + - мини-игры предназначены для изучения и повторения слов электронного учебника + - мини-игры "Саванна", "Аудиовызов" и "Спринт" повторяют одноимённые мини-игры приложения Lingualeo + - мини-игру с условным названием "Своя игра" вы придумываете сами + - слова, которые используются в мини-играх, отличаются в зависимости от того, откуда вы открываете игру: по ссылке в меню или по ссылке на странице учебника + - если мини-игра открывается по ссылке в меню, в ней есть возможность выбрать один из шести уровней сложности. Уровень сложности мини-игры определяет раздел учебника, слова из которого в ней будут использоваться, + - если мини-игра открывается по ссылке на странице учебника, она используется для повторения слов, размещённых на этой странице. В этом случае в игре нет возможности выбрать уровень сложности и используются те слова, которые размещены на данной странице учебника. Если 20 слов для мини-игры не хватает, в ней задействуются слова из предыдущих страниц учебника. Если предыдущих страниц нет или недостаточно, игру можно заканчивать досрочно, когда исчерпаются все доступные слова + - слова, которые использовались в мини-играх, открытых по ссылке на странице учебника или на странице раздела словаря "Сложные слова", попадают в раздел словаря "Изучаемые слова" + + #### Страница статистики + - на странице статистики отображается краткосрочная статистика по результатам каждого дня и долгосрочная статистика за весь период изучения + - в краткосрочной статистике указывается количество изученных слов, процент правильных ответов и самая длинная серия правильных ответов по каждой мини-игре отдельно, а также общее количество изученных слов и процент правильных ответов за день + - в долгосрочной статистике представлены два графика. На одном из них отображается количество изученных слов за каждый день изучения, на другом - увеличение общего количества изученных слов за весь период изучения по дням. + + #### Бекенд + + Что уже есть: + + создан репозиторий с бекендом на его основе создан ReactLearnWords API, позволяющий получить исходные данные + + создана ReactLearnWords wiki с инструкциями по созданию базы данных MongoDB, деплою бекенда на heroku, примерами получения исходных данных + + Что нужно сделать: + создать свою копию бекенда. Для этого: форкните репозиторий с бекендом для создания базы данных MongoDB и деплоя бекенда на heroku следуйте туториалам ReactLearnWords wiki + + - вам необходимо добавить в бекенд возможность при регистрации нового пользователя указать его имя и загрузить фото + +### Технические требования +- работа приложения проверяется в браузере Google Chrome последней версии +- необходимо использовать React +- можно использовать bootstrap, material design, css-фреймворки, html и css препроцессоры +- можно использовать js-библиотеки +- разрешается использовать jQuery только в качесте подключаемой зависимости для UI библиотек. Использование jQuery в основном коде приложения не допускается +- рекомендуется использовать TypeScript +- рекомендуется создать и использовать бекенд. Данная рекомендация связана с очень высоким спросом на фронтенд-разработчиков, знакомых хотя бы с основами node.js. +запрещено копировать код других студентов, демо, примеров, которые приводятся в задании. Этот запрет касается html, css, js кода. Можно использовать небольшие фрагменты кода со Stack Overflow, других самостоятельно найденных источников в интернете, за исключением github-репозиториев студентов курса. Возле использованного чужого фрагмента кода в комментарии указывается ссылка на источник. + +### Как сабмитить задание +Участникам команд необходимо записаться в таблицу, ссылка на которую будет размещена в анонсах + +Ссылку на pull request в rs app сабмитит только тимлид + +Убедитесь, что pull request доступен для проверки. Для этого откройте ссылку, которую сабмитите в rs app, в режиме инкогнито браузера + +Если задание не засабмитить до дедлайна, оно не попадёт на распределение при кросс-чеке и за него не будут выставлены баллы + +### Требования к оформлению приложения +#### особое внимание обратите на качество оформления приложения. +Как прототип можно использовать подходящие шаблоны, размещённые на behance, dribbble, pinterest + +Качественное приложение характеризуется проработанностью деталей, вниманием к типографике (не больше трёх шрифтов на странице, размер шрифта не меньше 14 рх, оптимальная контрастность шрифта и фона), тщательно подобранным контентом +вёрстка адаптивная. Минимальная ширина страницы, при которой проверяется корректность отображения приложения - 500рх + +Интерактивность элементов, с которыми пользователи могут взаимодействовать, изменение внешнего вида самого элемента и состояния курсора при наведении, использование разных стилей для активного и неактивного состояния элемента, плавные анимации +единство стилей всех страниц приложения - одинаковые шрифты, стили кнопок, отступы, одинаковые элементы на всех страницах приложения имеют одинаковый внешний вид и расположение. Цвет элементов и фоновые изображения могут отличаться. В этом случае цвета используются из одной палитры, а фоновые изображения из одной коллекции. + +#### Требования к мини-играм +- все игры выполнены в одном стиле, при этом в оформлении каждой игры есть индивидуальные отличия (цветовая схема, фоновый рисунок, эффекты анимации и т. д.) +мини-игру можно развернуть во весь экран +- по окончанию каждой игры выводятся результаты мини-игры +- одинаковые элементы игр, такие как результаты мини-игры, блок выбора уровня сложности, стартовый экран, если он есть, и т.д. идентичны по внешнему виду, расположению на странице, функционалу +- управлять игрой можно как мышкой, так и клавишами на клавиатуре, как это реализовано в оригинальных играх +- если мини-игра запускается из меню, в ней можно выбрать один из шести уровней сложности, которые отличаются тем, слова какой из шести частей коллекции исходных данных в ней задействованы +- если мини-игра запускается со страницы учебника, в ней используются слова из той страницы учебника, на которой размещена ссылка на игру. Если размещённых на странице слов для игры недостаточно, задействуются слова с предыдущих страниц + +### Критерии оценивания + +Максимальный балл за задание 600 + +500 баллов за приложение + +100 баллов за презентацию + +Для удобства проверки необходимо записать и разместить на YouTube небольшое (5-7 мин) видео для проверяющих с объяснением как реализован каждый пункт из перечисленных в критериях оценки. Особое внимание обратите на те пункты критериев оценки, которые проверяющий проверить не сможет, например, на то как вы реализовали базу данных, как задеплоили бекенд, как выглядит долгосрочная статистика и т.д. Ссылку на видео можно добавить в описание pull request или в footer приложения добавить иконку YouTube со ссылкой на видео. + +При оценивании приложения проверяются все требования, описанные в пунктах Описание функциональных блоков, Требования к оформлению приложения, Требования к мини-играм. Если какие-то из перечисленных требований не выполняются, снимаем часть баллов. В комментарии к оценке необходимо указать какие пункты не выполнены или выполнены частично. + +#### Вёрстка, дизайн, UI +40 +- вёрстка, дизайн, UI главной страницы приложения +10 +- вёрстка, дизайн, UI электронного учебника +10 +- вёрстка, дизайн, UI страницы статистики +10 +- оригинальный интересный качественный дизайн приложения +10 + +#### Главная страница приложения +40 +- меню +10 +- описание возможностей и преимуществ приложения +10 +- видео с демонстрацией работы приложения +10 +- раздел "О команде" +10 + +#### Электронный учебник +50 +- страницы и разделы учебника +10 +- настройки +10 +- список слов +20 +- навигация по страницам и разделам учебника +10 + +#### Словарь +40 +- раздел "Изучаемые слова" +20 +- раздел "Сложные слова" +10 +- раздел "Удалённые слова" +10 + +#### Мини-игры +200 (максимум +50 баллов за каждую игру) + +Мини-игра может оцениваться в 30, 40 или 50 баллов. + +При оценке предложенной командой игры, её сложность, интересность, полезность, качество реализации сравнивается с другими мини-играми и оценивается по сравнению с ними. + +- игра в основном соответствует прототипу, является его упрощённой версией +30 + +- игра полностью повторяет прототип и детали его работы. Выполняются все перечисленные в задании требования к мини-играм +40 + +- игра является улучшенной версией прототипа как с точки зрения внешнего вида и оформления, так и удобства работы. Присутствует дополнительный функционал, улучшающий качество приложения +50 + +#### Страница статистики +40 + +- краткосрочная статистика +20 + +- долгосрочная статистика +20 + +#### Бекенд +60 + +- собственная копия бекенда размещена на heroku или другом бесплатном хостинге +20 + +- приложение использует данные из собственного API +10 + +- при регистрации нового пользователя можно указать его имя. При перезагрузке клиента данные о пользователе сохраняются +10 + +- при регистрации нового пользователя можно загрузить фото +10 + +- реализована авторизация и разавторизация пользователя. Основная часть приложения доступна без авторизации. Авторизация необходима только для хранения долгосрочной статистики и формирования словаря +10 + +#### Дополнительный функционал +30** + +- реализован не указанный в задании дополнительный функционал. Оценивается оригинальная идея, вклад в улучшение качества приложения, полезность, сложность и качество выполнения +20 + +- написано не меньше 10 юнит-тестов, использующих различные методы jest +10 diff --git a/rslang/package.json b/rslang/package.json new file mode 100644 index 000000000..36293ac5d --- /dev/null +++ b/rslang/package.json @@ -0,0 +1,82 @@ +{ + "name": "rslang", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.9", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.8.1", + "@types/jest": "^26.0.20", + "@types/node": "^12.20.4", + "@types/react": "^17.0.2", + "@types/react-dom": "^17.0.1", + "@types/react-router-dom": "^5.1.7", + "bootstrap": "^4.6.0", + "bootstrap-icons": "^1.4.0", + "chart.js": "^2.9.4", + "lodash": "^4.17.21", + "node-sass": "^5.0.0", + "node-schedule": "^2.0.0", + "react": "^17.0.1", + "react-bootstrap": "^1.5.1", + "react-bootstrap-icons": "^1.4.0", + "react-chartjs-2": "^2.11.1", + "react-countdown-circle-timer": "^2.5.1", + "react-datetime": "^3.0.4", + "react-dom": "^17.0.1", + "react-gauge-chart": "^0.3.0", + "react-hook-form": "^6.15.5", + "react-icons": "^4.2.0", + "react-loadable": "^5.5.0", + "react-loadable-ssr-addon": "^1.0.1", + "react-moment": "^1.1.1", + "react-overlays": "^5.0.0", + "react-player": "^2.9.0", + "react-rating": "^2.0.5", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", + "react-scripts": "4.0.3", + "react-speech-recognition": "^3.7.0", + "react-yandex-maps": "^4.6.0", + "twix": "^1.3.0", + "typescript": "^4.2.3", + "web-vitals": "^1.1.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/lodash": "^4.14.168", + "@types/node-schedule": "^1.3.1", + "@types/react-gauge-chart": "^0.3.0", + "@types/react-loadable": "^5.5.4", + "@types/react-speech-recognition": "^3.6.0", + "@types/styled-components": "^5.1.9", + "react-animations": "^1.0.0", + "react-gauge-chart": "^0.3.0", + "react-hook-form": "^6.15.5", + "react-rating": "^2.0.5", + "styled-components": "^5.2.3" + } +} diff --git a/rslang/public/favicon.ico b/rslang/public/favicon.ico new file mode 100644 index 000000000..9f05b8bba Binary files /dev/null and b/rslang/public/favicon.ico differ diff --git a/rslang/public/index.html b/rslang/public/index.html new file mode 100644 index 000000000..745ec48c9 --- /dev/null +++ b/rslang/public/index.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + Rs-Lang-team108 + + + + +
+ + + + \ No newline at end of file diff --git a/rslang/public/manifest.json b/rslang/public/manifest.json new file mode 100644 index 000000000..5545e650f --- /dev/null +++ b/rslang/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "RsLang", + "name": "Lerning English with Rolling scope school", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/rslang/public/robots.txt b/rslang/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/rslang/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/rslang/public/sound/error.mp3 b/rslang/public/sound/error.mp3 new file mode 100644 index 000000000..6016de129 Binary files /dev/null and b/rslang/public/sound/error.mp3 differ diff --git a/rslang/src/App.scss b/rslang/src/App.scss new file mode 100644 index 000000000..c9ea4d55e --- /dev/null +++ b/rslang/src/App.scss @@ -0,0 +1,75 @@ +* { + padding: 0; + margin: 0; + border: 0; +} +*, +*:before, +*:after { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +:focus, +:active { + outline: none; +} +a:focus, +a:active { + outline: none; +} +nav, +footer, +header, +aside { + display: block; +} +html, +body { + height: 100%; + width: 100%; + font-size: 100%; + line-height: 1; + font-size: 14px; + -ms-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + font-family: "Montserrat", sans-serif; +} +input, +button, +textarea { + font-family: inherit; +} +input::-ms-clear { + display: none; +} +button { + cursor: pointer; +} +button::-moz-focus-inner { + padding: 0; + border: 0; +} +a, +a:visited { + text-decoration: none; +} +a:hover { + text-decoration: none; +} +ul li { + list-style: none; +} +img { + vertical-align: top; +} +// h1, +// h2, +// h3, +// h4, +// h5, +// h6 { +// font-size: inherit; +// font-weight: inherit; +// } diff --git a/rslang/src/App.test.tsx b/rslang/src/App.test.tsx new file mode 100644 index 000000000..2a68616d9 --- /dev/null +++ b/rslang/src/App.test.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/rslang/src/App.tsx b/rslang/src/App.tsx new file mode 100644 index 000000000..42da7db4f --- /dev/null +++ b/rslang/src/App.tsx @@ -0,0 +1,33 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./App.scss"; +import React from "react"; +import { BrowserRouter, Route, Switch } from "react-router-dom"; +import Loadable from "react-loadable"; +import LoadingScreen from "./components/LoadingScreen/LoadingScreen"; + +function App() { + const LoadableComponent = Loadable({ + loader: () => import("./components/HomePage/HomePage"), + loading: LoadingScreen, + delay: 300, + }); + const LoadableTutorialPage = Loadable({ + loader: () => import("./components/TutorialPage/TutorialPage"), + loading: LoadingScreen, + delay: 300, + }); + + return ( + +
+ + + + + + +
+
+ ); +} +export default App; diff --git a/rslang/src/api/defData.js b/rslang/src/api/defData.js new file mode 100644 index 000000000..5d4c197da --- /dev/null +++ b/rslang/src/api/defData.js @@ -0,0 +1,29 @@ +// export const url = "https://rocky-basin-33827.herokuapp.com/"; +// export const url = "http://serene-falls-78086.herokuapp.com/"; +export const url = "https://rslang.tk:3000/"; + +export const numOfPages = 30; + +export const defSettingsData = { + "wordsPerDay": 20, + "optional": {}, + "vocabulary": {strong: true, deleted: true, translate: true}, + "savanna": { sound: true, speak: true }, + "call": {}, + "sprint": {}, + "puzzle": {}, + "ourgame": {}, + "speakit": { sound: true } +}; + +export let defStatisticsData = { + "learnedWords": 0, + "optional": {}, + "vocabulary": {}, + "savanna": {}, + "call": {}, + "sprint": {}, + "puzzle": {}, + "ourgame": {}, + "speakit": {} +}; diff --git a/rslang/src/api/getUserData.ts b/rslang/src/api/getUserData.ts new file mode 100644 index 000000000..2cf218184 --- /dev/null +++ b/rslang/src/api/getUserData.ts @@ -0,0 +1,19 @@ +async function getUserDatas(url: string, bearerToken: string):Promise { + const init: RequestInit = { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${bearerToken}` + } + }; + const res = await fetch(url, init); + + if (!res.ok) { + throw new Error(`${url}, received ${res.status}`); + } + const body = await res.json(); + return body + } + +export default getUserDatas; \ No newline at end of file diff --git a/rslang/src/api/getWords.ts b/rslang/src/api/getWords.ts new file mode 100644 index 000000000..815f84cbc --- /dev/null +++ b/rslang/src/api/getWords.ts @@ -0,0 +1,18 @@ +async function getWords(url: string):Promise { + const init: RequestInit = { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + } + }; + const res = await fetch(url, init); + + if (!res.ok) { + throw new Error(`Could not fetch ${url}, received ${res.status}`); + } + const body = await res.json(); + return body + } + +export default getWords; \ No newline at end of file diff --git a/rslang/src/api/setUserData.ts b/rslang/src/api/setUserData.ts new file mode 100644 index 000000000..d799aa89a --- /dev/null +++ b/rslang/src/api/setUserData.ts @@ -0,0 +1,20 @@ +async function setUserDatas(url: string, bearerToken: string, data: Object):Promise { + const init: RequestInit = { + method: 'PUT', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${bearerToken}` + }, + body: JSON.stringify(data) + }; + const res = await fetch(url, init); + + if (!res.ok) { + throw new Error(`Could not fetch ${url}, received ${res.status}`); + } + const body = await res.json(); + return body + } + +export default setUserDatas; \ No newline at end of file diff --git a/rslang/src/assets/audio/correct.mp3 b/rslang/src/assets/audio/correct.mp3 new file mode 100644 index 000000000..62cafb037 Binary files /dev/null and b/rslang/src/assets/audio/correct.mp3 differ diff --git a/rslang/src/assets/audio/error.mp3 b/rslang/src/assets/audio/error.mp3 new file mode 100644 index 000000000..6016de129 Binary files /dev/null and b/rslang/src/assets/audio/error.mp3 differ diff --git a/rslang/src/assets/img/Run.jpg b/rslang/src/assets/img/Run.jpg new file mode 100644 index 000000000..3f751534e Binary files /dev/null and b/rslang/src/assets/img/Run.jpg differ diff --git a/rslang/src/assets/img/articles.jpg b/rslang/src/assets/img/articles.jpg new file mode 100644 index 000000000..cfe6a94cd Binary files /dev/null and b/rslang/src/assets/img/articles.jpg differ diff --git a/rslang/src/assets/img/ava_1.jpg b/rslang/src/assets/img/ava_1.jpg new file mode 100644 index 000000000..d704daad0 Binary files /dev/null and b/rslang/src/assets/img/ava_1.jpg differ diff --git a/rslang/src/assets/img/ava_2.jpg b/rslang/src/assets/img/ava_2.jpg new file mode 100644 index 000000000..b1b1e5d6e Binary files /dev/null and b/rslang/src/assets/img/ava_2.jpg differ diff --git a/rslang/src/assets/img/ava_3.jpg b/rslang/src/assets/img/ava_3.jpg new file mode 100644 index 000000000..65053629a Binary files /dev/null and b/rslang/src/assets/img/ava_3.jpg differ diff --git a/rslang/src/assets/img/ava_4.jpg b/rslang/src/assets/img/ava_4.jpg new file mode 100644 index 000000000..bf3f21050 Binary files /dev/null and b/rslang/src/assets/img/ava_4.jpg differ diff --git a/rslang/src/assets/img/background_features.jpg b/rslang/src/assets/img/background_features.jpg new file mode 100644 index 000000000..73348c875 Binary files /dev/null and b/rslang/src/assets/img/background_features.jpg differ diff --git a/rslang/src/assets/img/call.png b/rslang/src/assets/img/call.png new file mode 100644 index 000000000..bc0aa9896 Binary files /dev/null and b/rslang/src/assets/img/call.png differ diff --git a/rslang/src/assets/img/close.png b/rslang/src/assets/img/close.png new file mode 100644 index 000000000..60c5ac7fe Binary files /dev/null and b/rslang/src/assets/img/close.png differ diff --git a/rslang/src/assets/img/games.jpg b/rslang/src/assets/img/games.jpg new file mode 100644 index 000000000..7fef2342d Binary files /dev/null and b/rslang/src/assets/img/games.jpg differ diff --git a/rslang/src/assets/img/games/audio_rio.jpg b/rslang/src/assets/img/games/audio_rio.jpg new file mode 100644 index 000000000..7109f5f99 Binary files /dev/null and b/rslang/src/assets/img/games/audio_rio.jpg differ diff --git a/rslang/src/assets/img/games/savannah.jpg b/rslang/src/assets/img/games/savannah.jpg new file mode 100644 index 000000000..bfa7c7b57 Binary files /dev/null and b/rslang/src/assets/img/games/savannah.jpg differ diff --git a/rslang/src/assets/img/games/speakitImg.png b/rslang/src/assets/img/games/speakitImg.png new file mode 100644 index 000000000..6671cc5bb Binary files /dev/null and b/rslang/src/assets/img/games/speakitImg.png differ diff --git a/rslang/src/assets/img/games/sprintGame.jpg b/rslang/src/assets/img/games/sprintGame.jpg new file mode 100644 index 000000000..af376ba27 Binary files /dev/null and b/rslang/src/assets/img/games/sprintGame.jpg differ diff --git a/rslang/src/assets/img/hard_words.jpg b/rslang/src/assets/img/hard_words.jpg new file mode 100644 index 000000000..aa6bb0aaa Binary files /dev/null and b/rslang/src/assets/img/hard_words.jpg differ diff --git a/rslang/src/assets/img/main.jpg b/rslang/src/assets/img/main.jpg new file mode 100644 index 000000000..95bea538d Binary files /dev/null and b/rslang/src/assets/img/main.jpg differ diff --git a/rslang/src/assets/img/new_words.jpg b/rslang/src/assets/img/new_words.jpg new file mode 100644 index 000000000..92d18e6bd Binary files /dev/null and b/rslang/src/assets/img/new_words.jpg differ diff --git a/rslang/src/assets/img/our-game.png b/rslang/src/assets/img/our-game.png new file mode 100644 index 000000000..7234ff5c0 Binary files /dev/null and b/rslang/src/assets/img/our-game.png differ diff --git a/rslang/src/assets/img/progress.jpg b/rslang/src/assets/img/progress.jpg new file mode 100644 index 000000000..db11b63eb Binary files /dev/null and b/rslang/src/assets/img/progress.jpg differ diff --git a/rslang/src/assets/img/puzzle.png b/rslang/src/assets/img/puzzle.png new file mode 100644 index 000000000..63593f338 Binary files /dev/null and b/rslang/src/assets/img/puzzle.png differ diff --git a/rslang/src/assets/img/repeat_words.jpg b/rslang/src/assets/img/repeat_words.jpg new file mode 100644 index 000000000..b6b84dd59 Binary files /dev/null and b/rslang/src/assets/img/repeat_words.jpg differ diff --git a/rslang/src/assets/img/savanna.png b/rslang/src/assets/img/savanna.png new file mode 100644 index 000000000..252c1c015 Binary files /dev/null and b/rslang/src/assets/img/savanna.png differ diff --git a/rslang/src/assets/img/soundOff.jpg b/rslang/src/assets/img/soundOff.jpg new file mode 100644 index 000000000..b6c5d5c78 Binary files /dev/null and b/rslang/src/assets/img/soundOff.jpg differ diff --git a/rslang/src/assets/img/soundOn.jpg b/rslang/src/assets/img/soundOn.jpg new file mode 100644 index 000000000..f74fe57bf Binary files /dev/null and b/rslang/src/assets/img/soundOn.jpg differ diff --git a/rslang/src/assets/img/speak-it.png b/rslang/src/assets/img/speak-it.png new file mode 100644 index 000000000..6007d5f81 Binary files /dev/null and b/rslang/src/assets/img/speak-it.png differ diff --git a/rslang/src/assets/img/sprint.png b/rslang/src/assets/img/sprint.png new file mode 100644 index 000000000..f616f8c55 Binary files /dev/null and b/rslang/src/assets/img/sprint.png differ diff --git a/rslang/src/assets/img/statistics.jpg b/rslang/src/assets/img/statistics.jpg new file mode 100644 index 000000000..17dafeddc Binary files /dev/null and b/rslang/src/assets/img/statistics.jpg differ diff --git a/rslang/src/assets/img/tests.jpg b/rslang/src/assets/img/tests.jpg new file mode 100644 index 000000000..8a66461be Binary files /dev/null and b/rslang/src/assets/img/tests.jpg differ diff --git a/rslang/src/assets/img/tests_2.jpg b/rslang/src/assets/img/tests_2.jpg new file mode 100644 index 000000000..eb8632766 Binary files /dev/null and b/rslang/src/assets/img/tests_2.jpg differ diff --git a/rslang/src/assets/img/video.jpg b/rslang/src/assets/img/video.jpg new file mode 100644 index 000000000..c101418fd Binary files /dev/null and b/rslang/src/assets/img/video.jpg differ diff --git a/rslang/src/assets/img/words.jpg b/rslang/src/assets/img/words.jpg new file mode 100644 index 000000000..49f846b59 Binary files /dev/null and b/rslang/src/assets/img/words.jpg differ diff --git a/rslang/src/assets/sounds/correct.mp3 b/rslang/src/assets/sounds/correct.mp3 new file mode 100644 index 000000000..62cafb037 Binary files /dev/null and b/rslang/src/assets/sounds/correct.mp3 differ diff --git a/rslang/src/assets/sounds/error.mp3 b/rslang/src/assets/sounds/error.mp3 new file mode 100644 index 000000000..6016de129 Binary files /dev/null and b/rslang/src/assets/sounds/error.mp3 differ diff --git a/rslang/src/assets/svg/rss.svg b/rslang/src/assets/svg/rss.svg new file mode 100644 index 000000000..0e8b27c12 --- /dev/null +++ b/rslang/src/assets/svg/rss.svg @@ -0,0 +1,5 @@ + + +image2vector + + diff --git a/rslang/src/components/FullScreenWrapper/FullScreenWrapper.scss b/rslang/src/components/FullScreenWrapper/FullScreenWrapper.scss new file mode 100644 index 000000000..9ccc7ac00 --- /dev/null +++ b/rslang/src/components/FullScreenWrapper/FullScreenWrapper.scss @@ -0,0 +1,21 @@ +.fullscreen-icon { + cursor: pointer; + float: right; + + &_exit { + color: white; + } + + &:hover { + transform: scale(1.5, 1.5); + } +} + +.fullscreen-wrapper { + height: 100%; + + &__game { + display: flex; + height: 100%; + } +} diff --git a/rslang/src/components/FullScreenWrapper/FullScreenWrapper.tsx b/rslang/src/components/FullScreenWrapper/FullScreenWrapper.tsx new file mode 100644 index 000000000..4140bb7ab --- /dev/null +++ b/rslang/src/components/FullScreenWrapper/FullScreenWrapper.tsx @@ -0,0 +1,48 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import { ArrowsFullscreen, FullscreenExit } from "react-bootstrap-icons"; +import React, { ReactNode, useState } from "react"; + +import "./FullScreenWrapper.scss"; + +interface IFullScreenWrapperProps { + children: ReactNode; +} + +const FullScreenWrapper = ({ + children, +}: IFullScreenWrapperProps): JSX.Element => { + const [fullScreen, setFullScreen] = useState(false); + + const openFullscreen = (e: any): void => { + const galleryElement: any = e.target.parentElement; + if (galleryElement.requestFullscreen) { + setFullScreen(true); + galleryElement.requestFullscreen(); + } + }; + + const closeFullscreen = (): void => { + if (document.exitFullscreen) { + document.exitFullscreen(); + setFullScreen(false); + } + }; + + const fullScreenIcon: React.ReactNode = fullScreen ? ( + + ) : ( + + ); + + return ( +
+ {fullScreenIcon} +
{children}
+
+ ); +}; + +export default FullScreenWrapper; diff --git a/rslang/src/components/Games/AudioCall/AudioCall.scss b/rslang/src/components/Games/AudioCall/AudioCall.scss new file mode 100644 index 000000000..c34094e80 --- /dev/null +++ b/rslang/src/components/Games/AudioCall/AudioCall.scss @@ -0,0 +1,105 @@ +.audio-call { + min-height: 70vh; + width: 100%; + + &-game { + background: linear-gradient(#7d5db0, #c584a4); + width: 100%; + + &__wrapper { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + flex-direction: column; + } + } + + &__image { + width: 10vw; + margin-bottom: 3%; + height: 10vw; + } + + &__listen-btn { + background: rgba(255, 255, 255, 0.2); + border: unset; + border-radius: 50%; + + &_icon_big { + width: 8vw; + height: 8vw; + padding: 14%; + } + + &_icon { + width: 4vw; + height: 4vw; + padding: 4%; + } + } + + &__answer-btn { + width: 16%; + min-width: fit-content; + margin-top: 4%; + padding: 1%; + } + + &__answer { + display: flex; + align-items: center; + } + + &__answer-word { + font-size: 2rem; + color: white; + font-weight: bold; + margin-left: 3%; + } + + &__words { + margin-top: 4%; + font-size: 1.4rem; + display: flex; + flex-wrap: wrap; + width: 90%; + justify-content: center; + + &__word { + display: flex; + padding: 25px; + width: fit-content; + margin-right: 2%; + + &_active { + cursor: pointer; + &:hover { + background-color: rgba(255, 255, 255, 0.2); + } + } + + &_index { + padding-right: 12%; + color: rgba(255, 255, 255, 0.3); + + &_icon { + color: rgb(115, 253, 182); + } + } + &_text { + color: white; + &_inactive { + color: rgba(255, 255, 255, 0.3); + } + &_crossed { + text-decoration: line-through; + } + } + } + } +} + +.translucent-word-color { + color: rgba(255, 255, 255, 0.3); +} diff --git a/rslang/src/components/Games/AudioCall/AudioCall.tsx b/rslang/src/components/Games/AudioCall/AudioCall.tsx new file mode 100644 index 000000000..8214e0a83 --- /dev/null +++ b/rslang/src/components/Games/AudioCall/AudioCall.tsx @@ -0,0 +1,409 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./AudioCall.scss"; +import React, { useState, useEffect } from "react"; +import { VolumeUp, CheckCircleFill, ArrowRight } from "react-bootstrap-icons"; +import { Button, Image, ProgressBar } from "react-bootstrap"; + +import FullScreenWrapper from "./../../FullScreenWrapper/FullScreenWrapper"; +import Preview from "./../Preview/Preview"; +import RioImg from "../../../assets/img/games/audio_rio.jpg"; +import { url, numOfPages } from "../../../api/defData"; +import { playAudioWord, playAudio } from "../../../utils/AudioWord"; +import getWords from "../../../api/getWords"; +import Results from "./../Results/Results"; + +import getUserData from "../../../api/getUserData"; +import setUserData from "../../../api/setUserData"; + +const correctAudio = require("./../../../assets/audio/correct.mp3"); +const errorAudio = require("./../../../assets/audio/error.mp3"); + +const PREVIEW_HEADING = "Аудиовызов"; +const PREVIEW__DESCRIPTION = + "Ты слышишь слово и видишь 5 вариантов его перевода. При этом не видишь, как это слово пишется по-английски. Твоя задача выбрать правильный перевод озвученного слова."; + +type WordType = { + id: string; + word: string; + wordTranslate: string; + image: string; + audio: string; +}; + +type WordGameType = { + word: WordType; + isCorrect: boolean | null; + isPassed: boolean; + selectedWord: string | null; + displayedWords: string[]; +}; + +// type Statistics = { +// [key: string]: DayInfo; +// }; + +// type DayInfo = { +// correctAnswers: number; +// wrongAnswers: number; +// }; + +const AudioCall = () => { + const [words, setWords] = useState(null); + const [statisticsDone, setStatisticsDone] = useState(false); + const token = localStorage.getItem("token"); + const userId = localStorage.getItem("userId"); + const currentWord: WordGameType | undefined = words?.find( + (word) => word.isPassed === false + ); + + const level = null; //TODO: get level from book page + + const setUserWords = (words: WordType[]) => { + const gameWords: WordGameType[] = mapWordsToWordsGameType(words); + uploadFakeWords(gameWords); + }; + + const uploadFakeWords = async (gameWords: WordGameType[]): Promise => { + const randPage = Math.floor(Math.random() * numOfPages); + const levelRating = Math.floor(Math.random() * 6); + const fullUrl = `${url}words?page=${randPage}&group=${levelRating}`; + getWords(fullUrl) + .then((wordsData: any) => { + gameWords.map((gameWord) => { + const fakeWords = [ + ...getRandomFourWords(wordsData), + gameWord.word.wordTranslate, + ]; + gameWord.displayedWords = fakeWords.sort(() => 0.5 - Math.random()); + return gameWord; + }); + setWords(gameWords); + playAudioWord(gameWords[0].word.audio); + }) + .catch((error) => { + console.log(error.message); + }); + }; + + const setDefeatWord = () => { + saveAnswer(false, null); + }; + + const setAnswerWord = (selectedWord: any) => { + if (currentWord?.isCorrect !== null) return; + const isWin = selectedWord === currentWord?.word.wordTranslate; + saveAnswer(isWin, selectedWord); + }; + + const saveAnswer = (isCorrect: boolean, selectedWord: string | null): any => { + if (typeof words === undefined || words === null) return; + const updatedWords: WordGameType[] = words.map((oldWord: WordGameType) => { + if (oldWord.word.word === currentWord?.word.word) { + oldWord.isCorrect = isCorrect; + oldWord.selectedWord = selectedWord; + } + return oldWord; + }); + setWords(updatedWords); + if (isCorrect) { + playAudio(correctAudio.default); + } else { + playAudio(errorAudio.default); + } + }; + + async function updateUserStatistics( + correctAnswers: number, + wrongAnswers: number + ) { + if (token && userId) { + const fullUrl = `${url}users/${JSON.parse(userId)}/statistics`; + await getUserData(fullUrl, JSON.parse(token)) + .then((responseData: any) => { + const callData = responseData?.call || {}; + + const dateNow = new Date().getDate(); + const prevCorrectAnswers = callData[dateNow]?.correctAnswers || 0; + const prevWrongAnswers = callData[dateNow]?.wrongAnswers || 0; + + callData[dateNow] = { + correctAnswers: correctAnswers + prevCorrectAnswers, + wrongAnswers: wrongAnswers + prevWrongAnswers, + }; + + const fullUrl = `${url}users/${JSON.parse(userId)}/statistics`; + const bearerToken = JSON.parse(token); + if (!statisticsDone) { + setUserData(fullUrl, bearerToken, { call: callData }); + setStatisticsDone(true); + } + }) + .catch((error) => { + console.log(error.message); + }); + } + } + + useEffect(() => { + const handleUserKeyPress = (e: any) => { + switch (e.code) { + case "Digit1": { + setAnswerWord(currentWord?.displayedWords[0]); + break; + } + case "Digit2": { + setAnswerWord(currentWord?.displayedWords[1]); + break; + } + case "Digit3": { + setAnswerWord(currentWord?.displayedWords[2]); + break; + } + case "Digit4": { + setAnswerWord(currentWord?.displayedWords[3]); + break; + } + case "Digit5": { + setAnswerWord(currentWord?.displayedWords[4]); + break; + } + case "Enter": { + if (currentWord?.isCorrect === null) { + setDefeatWord(); + } else { + switchNextWord(); + } + break; + } + case "Space": { + if (currentWord) playAudioWord(currentWord.word.audio); + break; + } + default: + console.log(`${e.code} isn't supported for AudioCall game.`); + } + }; + + window.addEventListener("keydown", handleUserKeyPress); + + return () => { + window.removeEventListener("keydown", handleUserKeyPress); + }; + }, [words]); + + const switchNextWord = () => { + if (typeof words === undefined || words === null) return; + let wordIndex: number = 0; + const updatedWords: WordGameType[] = words.map( + (oldWord: WordGameType, index: number) => { + if (oldWord.word.word === currentWord?.word.word) { + oldWord.isPassed = true; + wordIndex = index; + } + return oldWord; + } + ); + setWords(updatedWords); + + if (words[wordIndex + 1]) { + playAudioWord(words[wordIndex + 1].word.audio); + } else { + } + }; + + const getProgress = () => { + const currentIndex = words?.indexOf(currentWord!); + return (currentIndex! / words!.length) * 100; + }; + + const continueGame = () => { + setStatisticsDone(false); + setWords(null); + }; + + const defineGameScene = () => { + if (words === null) { + return ( + + ); + } else if (currentWord === undefined) { + const correctWords = words + .filter((word) => word.isCorrect) + .map( + (correctWord) => + `${correctWord.word.word} - ${correctWord.word.wordTranslate}` + ); + + const wrongWords = words + .filter((word) => !word.isCorrect) + .map( + (wrongWord) => + `${wrongWord.word.word} - ${wrongWord.word.wordTranslate}` + ); + updateUserStatistics(correctWords.length, wrongWords.length); + + return ( +
+ +
+ ); + } else { + return ( +
+ +
+ {currentWord.isCorrect === null ? ( + + ) : ( + + )} +
+
+ ); + } + }; + + const gameScene = defineGameScene(); + + return ( +
+ {gameScene} +
+ ); +}; + +const TaskMode = ({ currentWord, setDefeatWord, setAnswerWord }: any) => { + return ( + + + + + + ); +}; + +const AnswerMode = ({ currentWord, switchNextWord, setAnswerWord }: any) => { + return ( + + +
+ + {currentWord.word.word} +
+ + + +
+ ); +}; + +const AudioElement = ({ isAnswer, currentWord }: any) => { + const listenWord = () => { + if (currentWord) playAudioWord(currentWord.word.audio); + }; + + const volumeUpIconClass = isAnswer + ? "audio-call__listen-btn_icon" + : "audio-call__listen-btn_icon_big"; + return ( + + ); +}; + +const WordsElement = ({ currentWord, setAnswerWord }: any) => { + const wordElements = currentWord?.displayedWords.map( + (word: string, index: number) => { + let wordClasses = "audio-call__words__word_text"; + let indexElement: number | null = index + 1; + if (currentWord.isCorrect !== null) { + if (currentWord.word.wordTranslate === word) { + if (currentWord.isCorrect) { + indexElement = null; + } + } else { + wordClasses += "_inactive"; + if (currentWord.selectedWord === word) { + wordClasses += " audio-call__words__word_text_crossed"; + } + } + } + let wordWrapperClasses = "audio-call__words__word"; + if (currentWord.isCorrect === null) { + wordWrapperClasses += " audio-call__words__word_active"; + } + return ( +
+ setAnswerWord(e.currentTarget.lastElementChild!.textContent) + } + key={index} + > + + {indexElement ? ( + indexElement + ) : ( + + )} + + {word} +
+ ); + } + ); + + return
{wordElements}
; +}; + +const getRandomFourWords = (wordArr: WordType[]) => { + const shuffled = wordArr.sort(() => 0.5 - Math.random()); + return shuffled.slice(0, 4).map((shuffledWord) => shuffledWord.wordTranslate); +}; + +const mapWordsToWordsGameType = (words: WordType[]): WordGameType[] => { + return words.map((word: WordType) => { + return { word: word, isCorrect: null, isPassed: false } as WordGameType; + }); +}; + +export default AudioCall; diff --git a/rslang/src/components/Games/Games.scss b/rslang/src/components/Games/Games.scss new file mode 100644 index 000000000..ae5bb5bd0 --- /dev/null +++ b/rslang/src/components/Games/Games.scss @@ -0,0 +1,56 @@ +.img-mini-game { + width: 130px; + margin: 20px auto 0px auto; +} + +.text-mini-game { + font-size: 25px; + font-family: cursive; + text-align: center; + color: white; +} + +.btn-mini-game { + margin: 15px 82px; + font-size: 16px; + border-radius: 15px; + background-color: #117a8b; + font-family: cursive; +} + +.games-link { + margin: 0 30px; +} + +.card-mini-game { + border-radius: 50px; + animation: all 0.3s; +} + +.card-mini-game:hover { + transform: scale(1.05); +} + +.card-mini-game-1 { + background-color: rgb(252, 214, 2); +} + +.card-mini-game-2 { + background-color: rgb(42, 72, 206); +} + +.card-mini-game-3 { + background-color: rgb(90, 221, 38); +} + +.card-mini-game-4 { + background-color: rgb(230, 23, 137); +} + +.card-mini-game-5 { + background-color: rgb(35, 226, 178); +} + +.card-mini-game-6 { + background-color: rgb(228, 42, 42); +} \ No newline at end of file diff --git a/rslang/src/components/Games/Games.tsx b/rslang/src/components/Games/Games.tsx new file mode 100644 index 000000000..adaf252d5 --- /dev/null +++ b/rslang/src/components/Games/Games.tsx @@ -0,0 +1,94 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Games.scss"; +import React from "react"; +import { Route, Switch } from "react-router-dom"; +import { Link } from "react-router-dom"; +import { Container, Card, Button } from "react-bootstrap"; +import Sprint from "../../assets/img/sprint.png"; +import Puzzle from "../../assets/img/puzzle.png"; +import Call from "../../assets/img/call.png"; +import OurGameImg from "../../assets/img/our-game.png"; +import Savanna from "../../assets/img/savanna.png"; +import SpeakIt from "../../assets/img/speak-it.png"; + +import AudioCall from "./AudioCall/AudioCall"; +import Savannah from "./Savanna/Savanna"; +import SprintGame from "./Sprint/SprintGame"; +import Speakit from "./Speakit/Speakit"; + +interface InterfaceGames {} + +type GameCardType = { + name: string; + image: any; + url: string; + styleClass: string; +}; + +const Games: React.FC = (props) => { + const basePathName = "/tutorial-page/games"; + const gameCards: GameCardType[] = [ + { + name: "Саванна", + image: Savanna, + url: "savanna", + styleClass: "card-mini-game-3", + }, + { + name: "Аудиовызов", + image: Call, + url: "call", + styleClass: "card-mini-game-4", + }, + { + name: "Спринт", + image: Sprint, + url: "sprint", + styleClass: "card-mini-game-5", + }, + { + name: "Скажи это", + image: SpeakIt, + url: "speak-it", + styleClass: "card-mini-game-1", + } + ]; + + const gameCardElements = gameCards.map((card, index) => { + const styleClasses = `border-0 shadow bg-body m-3 card-mini-game ${card.styleClass}`; + return ( + + + + + {card.name} + + + + + ); + }); + + return ( + + + + + + + + + + + + + + + + {gameCardElements} + + + + ); +}; +export default Games; diff --git a/rslang/src/components/Games/OurGame/OurGame copy.tsx b/rslang/src/components/Games/OurGame/OurGame copy.tsx new file mode 100644 index 000000000..c508ba171 --- /dev/null +++ b/rslang/src/components/Games/OurGame/OurGame copy.tsx @@ -0,0 +1,250 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import './OurGame.scss'; +import { useState, useEffect } from "react"; +import { + Button, + ButtonGroup, + ButtonToolbar, + Toast, + Card, + Modal, + ProgressBar } from "react-bootstrap"; +import { BsVolumeMute, BsFillVolumeUpFill, BsArrowRepeat } from "react-icons/bs"; +import { BiBell, BiBellOff, BiExit } from "react-icons/bi"; + +import FullScreenWrapper from "../../FullScreenWrapper/FullScreenWrapper"; +import { Redirect } from "react-router"; +import Preview from "../Preview/Preview"; +import getUserData from "../../../api/getUserData"; +import setUserData from "../../../api/setUserData"; +import SavannahImg from "../../../assets/img/games/savannah.jpg"; +import { url } from "../../../api/defData"; + +type Statistics = { correctAnswers: number; wrongAnswers: number }; +type Settings = { sound: boolean; speak: boolean }; +type AllStatistics = { [index: number]: Statistics }; +type AllSettings = { [index: string]: Settings }; +type wordsAnswersType = { [index: string]: string }; + +type word = { + audio: string, + audioExample: string, + audioMeaning: string, + group: number, + id: string, + image: string, + page: number, + textExample: string, + textExampleTranslate: string, + textMeaning: string, + textMeaningTranslate: string, + transcription: string, + word: string, + wordTranslate: string, +}; + +const OurGame = () => { +const PREVIEW_HEADING = "Концентрация"; +const PREVIEW__DESCRIPTION = + "Найди все верные пары Слово - Перевод, переворачивая карточки."; +const NUM_OF_ANSWERS = 10; +const TIMEOUT_TIME = 450; +const NUM_OF_ATTEMPTS = 20; +const dateNow = new Date().getDate(); +const defAllStatistics: AllStatistics = { + [dateNow]: { correctAnswers: 0, wrongAnswers: 0 }, +}; + +const settingsLocal:string | null = localStorage.getItem('settings'); + const locSettings:AllSettings = settingsLocal ? JSON.parse(settingsLocal) : {savanna: {sound: true, speak: true}}; + const settings:Settings = locSettings.savanna; + const {sound, speak} = settings; + const [words, setWords] = useState | null>(null); + const [wordsSet, setWordsSet] = useState | null>(null); + const [level, setLevel] = useState(null); //TODO: get level from book page + const [soundOff, setSoundOff] = useState(sound); + const [isSpeak, setIsSpeak] = useState(speak); + const [timeoutTime, setTimeoutTime] = useState(TIMEOUT_TIME); + const [timerStart, setTimerStart] = useState(false); + const [showModal, setShowModal] = useState(false); + const [wordsCards, setWordsCards] = useState([<>]); + const [wordsAnswers, setWordsAnswers] = useState({}); + const [lives, setLives] = useState(100); + const [attempt, setAttempt] = useState(0); + const [statistics, setStatistics] = useState(defAllStatistics[dateNow]); + const [buttons, setButtons] = useState([ + "Ошибка", + "получения", + "слов", + "с", + "сервера", + ]); + const [wrongAnswersWords, setWrongAnswersWords] = useState>([]); + const [exit, setExit] = useState(false); + const token = localStorage.getItem("token"); + const userId = localStorage.getItem("userId"); + + const handleClose = () => { + setShowModal(false); + }; + + const handleShow = () => setShowModal(true); + + const setUserWords = (words: any) => { + setWords(words); + // getUserStatistics(); + }; + + const shuffleWords = (words: any) => { + if (words) { + let j, temp; + for (let i = words.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + temp = words[j]; + words[j] = words[i]; + words[i] = temp; + } + return words; + } + }; + + useEffect(() => { + if (words) { + setWordsSet(shuffleWords(words)); + } + }, [words]) + + useEffect(() => { + if (wordsSet) { + const wordsCards:JSX.Element[] = []; + const wordsAnswers:wordsAnswersType = {}; + wordsSet.forEach((word: word, i) => { + const wordCard = + ( {console.log(e.target)}}> + + {word.word} + + ); + + const wordTrCard = (<> + {console.log(e.target)}}> + + {word.wordTranslate} + + ); + wordsCards.push(wordCard); + wordsAnswers[word.word] = word.wordTranslate; + }); + setWordsAnswers(wordsAnswers); + setWordsCards(shuffleWords(wordsCards)) + } + }, [wordsSet]) + + const ModalFrame = ( + + + Игра окончена + + +
+

Ваш результат:

+
+

Верных ответов:

+ {statistics.correctAnswers} +

Неверных ответов:

+ {statistics.wrongAnswers} +
+

Необходимо повторить слова:

+
{wrongAnswersWords}
+
+
+ + + + + +
+ ); + + const buttonsBar = ( + + + + + + + + + + + + + + + ); + + const progressBar = <>; + const attemptsBar = <>; + + const gameWrapper = ( +
+ {wordsCards} +
+ ); + + const Game = ( + <> +
+ {progressBar} +
+ {buttonsBar} +
+
+ {gameWrapper} + + ); + +return ( +
+ + {exit && } + {ModalFrame} + {words === null ? ( + + ) : ( +
+ {!wordsSet && "Набор слов отсутствует"} + {wordsSet && Game} +
+ )} +
+
+ ); +}; + +export default OurGame; \ No newline at end of file diff --git a/rslang/src/components/Games/OurGame/OurGame.scss b/rslang/src/components/Games/OurGame/OurGame.scss new file mode 100644 index 000000000..d658f1a5e --- /dev/null +++ b/rslang/src/components/Games/OurGame/OurGame.scss @@ -0,0 +1,54 @@ +.ourgame { + min-height: 70vh; + min-width: 100%; + + .menu { + margin-bottom: 10px; + + .menu-wrapper { + display: flex; + justify-content: space-between; + + .btns-toolbar{ + margin: 5px; + width: 100%; + + .attempts{ + margin: 10px 10px 10px auto; + width: 30%; + height: 30px; + border-radius: 10px; + font-size: 1.5rem; + } + + .btn-group { + margin: 0 10px; + } + } + } + } + + .ourgame-cards { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + position: relative; + height: 85%; + width: 100%; + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + padding: 5px; + + .card { + text-align: center; + width: 230px; + margin: 10px; + font-size: 1.5rem; + padding: 2px; + } + } + +}; \ No newline at end of file diff --git a/rslang/src/components/Games/OurGame/OurGame.tsx b/rslang/src/components/Games/OurGame/OurGame.tsx new file mode 100644 index 000000000..5b22eae55 --- /dev/null +++ b/rslang/src/components/Games/OurGame/OurGame.tsx @@ -0,0 +1,269 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import './OurGame.scss'; +import { useState, useEffect } from "react"; +import { + Button, + ButtonGroup, + ButtonToolbar, + Toast, + Card, + Modal, + ProgressBar } from "react-bootstrap"; +import { BsVolumeMute, BsFillVolumeUpFill, BsArrowRepeat } from "react-icons/bs"; +import { BiBell, BiBellOff, BiExit } from "react-icons/bi"; +import { CountdownCircleTimer } from 'react-countdown-circle-timer'; + +import FullScreenWrapper from "../../FullScreenWrapper/FullScreenWrapper"; +import { Redirect } from "react-router"; +import Preview from "../Preview/Preview"; +import getUserData from "../../../api/getUserData"; +import setUserData from "../../../api/setUserData"; +import SavannahImg from "../../../assets/img/games/savannah.jpg"; +import { url } from "../../../api/defData"; + +type Statistics = { correctAnswers: number; wrongAnswers: number }; +type Settings = { sound: boolean; speak: boolean }; +type AllStatistics = { [index: number]: Statistics }; +type AllSettings = { [index: string]: Settings }; +type wordsAnswersType = { [index: string]: string }; + +type word = { + audio: string, + audioExample: string, + audioMeaning: string, + group: number, + id: string, + image: string, + page: number, + textExample: string, + textExampleTranslate: string, + textMeaning: string, + textMeaningTranslate: string, + transcription: string, + word: string, + wordTranslate: string, +}; + +const OurGame = () => { +const PREVIEW_HEADING = "Концентрация"; +const PREVIEW__DESCRIPTION = + "Найди все верные пары Слово - Перевод, переворачивая карточки."; +const NUM_OF_ANSWERS = 10; +const TIMEOUT_TIME = 450; +const NUM_OF_ATTEMPTS = 20; +const dateNow = new Date().getDate(); +const defAllStatistics: AllStatistics = { + [dateNow]: { correctAnswers: 0, wrongAnswers: 0 }, +}; +const defWordsCards = (
Нет ничего
) + +const settingsLocal:string | null = localStorage.getItem('settings'); + const locSettings:AllSettings = settingsLocal ? JSON.parse(settingsLocal) : {savanna: {sound: true, speak: true}}; + const settings:Settings = locSettings.ourgame; + const {sound, speak} = settings; + const [words, setWords] = useState | null>(null); + const [wordsSet, setWordsSet] = useState | null>(null); + const [level, setLevel] = useState(null); //TODO: get level from book page + const [soundOff, setSoundOff] = useState(sound); + const [isSpeak, setIsSpeak] = useState(speak); + const [timeoutTime, setTimeoutTime] = useState(TIMEOUT_TIME); + const [timerStart, setTimerStart] = useState(false); + const [showModal, setShowModal] = useState(false); + const [wordsCards, setWordsCards] = useState(null); + const [wordsAnswers, setWordsAnswers] = useState({}); + const [lives, setLives] = useState(100); + const [attempt, setAttempt] = useState(0); + const [statistics, setStatistics] = useState(defAllStatistics[dateNow]); + const [buttons, setButtons] = useState([ + "Ошибка", + "получения", + "слов", + "с", + "сервера", + ]); + const [wrongAnswersWords, setWrongAnswersWords] = useState>([]); + const [exit, setExit] = useState(false); + const token = localStorage.getItem("token"); + const userId = localStorage.getItem("userId"); + + const handleClose = () => { + setShowModal(false); + }; + + const handleShow = () => setShowModal(true); + + const setUserWords = (words: any) => { + setWords(words); + // getUserStatistics(); + }; + + const shuffleWords = (words: any) => { + if (words) { + let j, temp; + for (let i = words.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + temp = words[j]; + words[j] = words[i]; + words[i] = temp; + } + return words; + } + }; + + useEffect(() => { + if (words) { + setWordsSet(shuffleWords(words)); + } + }, [words]) + + useEffect(() => { + if (wordsSet) { + const slicedWordSet = wordsSet.slice(0, NUM_OF_ANSWERS); + const wordsCards:JSX.Element[] = []; + const wordsAnswers:wordsAnswersType = {}; + slicedWordSet.forEach((word: word, i) => { + const wordCard = + ( {console.log(e.target)}}> + + {word.word} + + ); + + const wordTrCard = + ( {console.log(e.target)}}> + + {word.wordTranslate} + + ); + + wordsCards.push(wordCard, wordTrCard); + wordsAnswers[word.word] = word.wordTranslate; + }); + setWordsAnswers(wordsAnswers); + setWordsCards(shuffleWords(wordsCards)) + } + }, [wordsSet]) + + const ModalFrame = ( + + + Игра окончена + + +
+

Ваш результат:

+
+

Верных ответов:

+ {statistics.correctAnswers} +

Неверных ответов:

+ {statistics.wrongAnswers} +
+

Необходимо повторить слова:

+
{wrongAnswersWords}
+
+
+ + + + + +
+ ); + + const attemptsBar = <>; + const progressBar = <>; + + const UrgeWithPleasureComponent = () => ( + + {({ remainingTime }) => remainingTime} + + ) + const buttonsBar = ( + + + + + + + + + + + + + + {UrgeWithPleasureComponent} + {attemptsBar} + + ); + + const gameWrapper = ( +
+ {wordsCards} +
+ ); + + const Game = ( + <> +
+ {progressBar} +
+ {buttonsBar} +
+
+ {gameWrapper} + + ); + +return ( +
+ + {exit && } + {ModalFrame} + {words === null ? ( + + ) : ( +
+ {!wordsSet && "Набор слов отсутствует"} + {wordsSet && Game} +
+ )} +
+
+ ); +}; + +export default OurGame; \ No newline at end of file diff --git a/rslang/src/components/Games/Preview/Preview.scss b/rslang/src/components/Games/Preview/Preview.scss new file mode 100644 index 000000000..0a70fc006 --- /dev/null +++ b/rslang/src/components/Games/Preview/Preview.scss @@ -0,0 +1,37 @@ +.preview { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + position: relative; + height: 100%; + width: 100%; + + display: flex; + justify-content: center; + align-items: center; + + &__jumbotron { + color: white; + font-weight: 600; + text-align: center; + + &__heading { + font-size: 4rem; + } + + &__description { + font-size: 1.2rem; + } + + &__level { + display: flex; + justify-content: center; + align-items: center; + font-size: 1.2rem; + + &__title { + margin-right: 1%; + } + } + } +} diff --git a/rslang/src/components/Games/Preview/Preview.tsx b/rslang/src/components/Games/Preview/Preview.tsx new file mode 100644 index 000000000..743430447 --- /dev/null +++ b/rslang/src/components/Games/Preview/Preview.tsx @@ -0,0 +1,92 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Preview.scss"; +import React, { useState } from "react"; +import { Jumbotron, Button, OverlayTrigger, Tooltip } from "react-bootstrap"; +import Rating from "react-rating"; +import getWords from "../../../api/getWords"; +import { url, numOfPages } from "../../../api/defData"; + +type PreviewProps = { + backgroundImg: any; + heading: string; + description: string; + level: number | null; + setUserWords: (words: any) => void; +}; + +const Preview = ({ + backgroundImg, + heading, + description, + level, + setUserWords, +}: PreviewProps) => { + const [levelRating, setLevelRating] = useState(level || 0); + const [wordsPage, setWordsPage] = useState(0); + + const onStarRatingPress = (newLevel: any) => { + setLevelRating(newLevel); + const randPage = Math.floor(Math.random() * numOfPages) + setWordsPage(randPage) + }; + + const uploadWords = async ():Promise => { + const fullUrl = `${url}words?page=${wordsPage}&group=${levelRating-1}`; + getWords(fullUrl).then((wordsData:any) => { + setUserWords(wordsData); + }).catch(error => { + console.log(error.message) + }); + //TODO: add api call for getting words by levelRating + return 'data' + }; + + const startBtn = + levelRating === 0 ? ( + + Для продолжения требуется выбрать уровень сложности + + } + > + + + + + ) : ( + + ); + + return ( +
+ +

{heading}

+

{description}

+

+ + Уровень сложности + + +

+

{startBtn}

+
+
+ ); +}; + +export default Preview; diff --git a/rslang/src/components/Games/Preview/assets/img/articles.jpg b/rslang/src/components/Games/Preview/assets/img/articles.jpg new file mode 100644 index 000000000..cfe6a94cd Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/articles.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/ava_1.jpg b/rslang/src/components/Games/Preview/assets/img/ava_1.jpg new file mode 100644 index 000000000..d704daad0 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/ava_1.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/ava_2.jpg b/rslang/src/components/Games/Preview/assets/img/ava_2.jpg new file mode 100644 index 000000000..b1b1e5d6e Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/ava_2.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/ava_3.jpg b/rslang/src/components/Games/Preview/assets/img/ava_3.jpg new file mode 100644 index 000000000..65053629a Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/ava_3.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/ava_4.jpg b/rslang/src/components/Games/Preview/assets/img/ava_4.jpg new file mode 100644 index 000000000..bf3f21050 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/ava_4.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/background_features.jpg b/rslang/src/components/Games/Preview/assets/img/background_features.jpg new file mode 100644 index 000000000..73348c875 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/background_features.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/call.png b/rslang/src/components/Games/Preview/assets/img/call.png new file mode 100644 index 000000000..bc0aa9896 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/call.png differ diff --git a/rslang/src/components/Games/Preview/assets/img/games.jpg b/rslang/src/components/Games/Preview/assets/img/games.jpg new file mode 100644 index 000000000..7fef2342d Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/games.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/games/audio_rio.jpg b/rslang/src/components/Games/Preview/assets/img/games/audio_rio.jpg new file mode 100644 index 000000000..7109f5f99 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/games/audio_rio.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/games/savannah.jpg b/rslang/src/components/Games/Preview/assets/img/games/savannah.jpg new file mode 100644 index 000000000..bfa7c7b57 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/games/savannah.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/hard_words.jpg b/rslang/src/components/Games/Preview/assets/img/hard_words.jpg new file mode 100644 index 000000000..aa6bb0aaa Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/hard_words.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/main.jpg b/rslang/src/components/Games/Preview/assets/img/main.jpg new file mode 100644 index 000000000..95bea538d Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/main.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/new_words.jpg b/rslang/src/components/Games/Preview/assets/img/new_words.jpg new file mode 100644 index 000000000..92d18e6bd Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/new_words.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/our-game.png b/rslang/src/components/Games/Preview/assets/img/our-game.png new file mode 100644 index 000000000..7234ff5c0 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/our-game.png differ diff --git a/rslang/src/components/Games/Preview/assets/img/progress.jpg b/rslang/src/components/Games/Preview/assets/img/progress.jpg new file mode 100644 index 000000000..db11b63eb Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/progress.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/puzzle.png b/rslang/src/components/Games/Preview/assets/img/puzzle.png new file mode 100644 index 000000000..63593f338 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/puzzle.png differ diff --git a/rslang/src/components/Games/Preview/assets/img/repeat_words.jpg b/rslang/src/components/Games/Preview/assets/img/repeat_words.jpg new file mode 100644 index 000000000..b6b84dd59 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/repeat_words.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/savanna.png b/rslang/src/components/Games/Preview/assets/img/savanna.png new file mode 100644 index 000000000..252c1c015 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/savanna.png differ diff --git a/rslang/src/components/Games/Preview/assets/img/speak-it.png b/rslang/src/components/Games/Preview/assets/img/speak-it.png new file mode 100644 index 000000000..6007d5f81 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/speak-it.png differ diff --git a/rslang/src/components/Games/Preview/assets/img/sprint.png b/rslang/src/components/Games/Preview/assets/img/sprint.png new file mode 100644 index 000000000..f616f8c55 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/sprint.png differ diff --git a/rslang/src/components/Games/Preview/assets/img/statistics.jpg b/rslang/src/components/Games/Preview/assets/img/statistics.jpg new file mode 100644 index 000000000..17dafeddc Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/statistics.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/tests.jpg b/rslang/src/components/Games/Preview/assets/img/tests.jpg new file mode 100644 index 000000000..8a66461be Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/tests.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/tests_2.jpg b/rslang/src/components/Games/Preview/assets/img/tests_2.jpg new file mode 100644 index 000000000..eb8632766 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/tests_2.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/video.jpg b/rslang/src/components/Games/Preview/assets/img/video.jpg new file mode 100644 index 000000000..c101418fd Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/video.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/img/words.jpg b/rslang/src/components/Games/Preview/assets/img/words.jpg new file mode 100644 index 000000000..49f846b59 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/img/words.jpg differ diff --git a/rslang/src/components/Games/Preview/assets/sounds/correct.mp3 b/rslang/src/components/Games/Preview/assets/sounds/correct.mp3 new file mode 100644 index 000000000..62cafb037 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/sounds/correct.mp3 differ diff --git a/rslang/src/components/Games/Preview/assets/sounds/error.mp3 b/rslang/src/components/Games/Preview/assets/sounds/error.mp3 new file mode 100644 index 000000000..6016de129 Binary files /dev/null and b/rslang/src/components/Games/Preview/assets/sounds/error.mp3 differ diff --git a/rslang/src/components/Games/Preview/assets/svg/rss.svg b/rslang/src/components/Games/Preview/assets/svg/rss.svg new file mode 100644 index 000000000..0e8b27c12 --- /dev/null +++ b/rslang/src/components/Games/Preview/assets/svg/rss.svg @@ -0,0 +1,5 @@ + + +image2vector + + diff --git a/rslang/src/components/Games/Results/Results.scss b/rslang/src/components/Games/Results/Results.scss new file mode 100644 index 000000000..31680417a --- /dev/null +++ b/rslang/src/components/Games/Results/Results.scss @@ -0,0 +1,86 @@ +.results { + margin: 5%; + height: 88%; + width: 90%; + border-radius: 5px 5px 5px 5px; + background-color: white; + display: flex; + flex-direction: column; + align-items: center; + + &__final-tittle { + font-size: 2rem; + margin: 7%; + } + + &__info { + height: 33vh; + overflow: scroll; + } + + &__tabs-control { + display: flex; + width: 100%; + justify-content: center; + + &__dot { + height: 12px; + width: 12px; + border-radius: 50%; + cursor: pointer; + background-color: rgba(0, 0, 0, 0.2); + margin-right: 1%; + + &-active { + background-color: #2582e7; + } + } + } + + &__continue_btn { + background-color: #eef5fd; + color: #2582e7; + border: none; + font-weight: bold; + margin: 3%; + &:hover { + background-color: #4d77a4; + } + } + + &__list-games { + margin-bottom: 5%; + } +} + +.fullness { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-evenly; + align-items: center; + + &__link { + font-size: 1.2rem; + color: #2582e7; + cursor: pointer; + text-decoration: underline; + } +} + +.words-info { + min-width: 30vw; + &__words { + margin-left: 5%; + } + + &__wrong { + color: red; + font-weight: bold; + } + + &__correct { + color: green; + font-weight: bold; + } +} diff --git a/rslang/src/components/Games/Results/Results.tsx b/rslang/src/components/Games/Results/Results.tsx new file mode 100644 index 000000000..a6f1cb52c --- /dev/null +++ b/rslang/src/components/Games/Results/Results.tsx @@ -0,0 +1,95 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Results.scss"; + +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; +import GaugeChart from "react-gauge-chart"; +import { Link } from "react-router-dom"; + +type ResultsProps = { + correctWords: string[]; + wrongWords: string[]; + continueGame: () => void; +}; + +const Results = ({ correctWords, wrongWords, continueGame }: ResultsProps) => { + const [isOverviewMode, setIsOverviewMode] = useState(true); + + const toggleOverviewMode = () => { + const currentState = isOverviewMode; + setIsOverviewMode(!currentState); + }; + const percent = + correctWords.length / (correctWords.length + wrongWords.length); + + const dotClass = "results__tabs-control__dot"; + const dotClassActive = dotClass + " results__tabs-control__dot-active"; + + const correctWordElements = correctWords.map((word, index) => ( +

+ {word} +

+ )); + const wrongWordElements = wrongWords.map((word, index) => ( +

+ {word} +

+ )); + + return ( +
+

+ Неплохо, но есть над чем поработать +

+
+ {isOverviewMode ? ( +
+

+ {correctWords.length} слов изучено, {wrongWords.length} на + изучении +

+ +
+ ) : ( +
+

ОШИБОК: {wrongWords.length}

+ {wrongWordElements} +
+

ЗНАЮ: {correctWords.length}

+ {correctWordElements} +
+ )} +
+
+
+
+
+ + +

К списку тренировок

+ +
+ ); +}; + +export default Results; diff --git a/rslang/src/components/Games/Savanna/Savanna.scss b/rslang/src/components/Games/Savanna/Savanna.scss new file mode 100644 index 000000000..f60107a99 --- /dev/null +++ b/rslang/src/components/Games/Savanna/Savanna.scss @@ -0,0 +1,107 @@ +.savanna { + min-height: 70vh; + min-width: 100%; + + .savanna-game { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + position: relative; + height: 100%; + width: 100%; + border-radius: 10px; + } + + .menu { + margin-bottom: 10px; + + .menu-wrapper { + display: flex; + justify-content: space-between; + + .rating{ + margin: 10px; + width: 200px; + height: 30px; + border-radius: 10px; + font-size: 1.5rem; + } + + .btns-toolbar{ + margin: 5px; + + .btn-group { + margin: 0 10px; + } + } + } + } + + .game-wrapper { + height: 85%; + width: 100%; + display: flex; + flex-wrap: wrap; + overflow: hidden; + + .question-wrapper{ + height: 100%; + width: 50%; + text-align: center; + overflow: hidden; + } + + .answers-wrapper{ + height: 100%; + width: 50%; + + .answers-btns { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + + .word-answer{ + display: flex; + height: fit-content; + min-width: fit-content; + text-transform: uppercase; + width: 15%; + margin: 7% 5%; + font-size: 1.3rem; + padding: 7px + } + + .button-icons { + margin-right: auto; + display: flex; + justify-content: space-between; + align-items: center; + + + .hotkey { + width: 25px; + height: 25px; + font-weight: 450; + background-color: #777; + border-radius: 5px; + margin-right: 10px; + } + } + + } + } + } + + .word-quest{ + font-size: 2rem; + position: relative; + left: 0vw; + border-radius: 30px; + text-transform: uppercase; + padding: 20px; + overflow: hidden; + } + +} \ No newline at end of file diff --git a/rslang/src/components/Games/Savanna/Savanna.tsx b/rslang/src/components/Games/Savanna/Savanna.tsx new file mode 100644 index 000000000..f085f351e --- /dev/null +++ b/rslang/src/components/Games/Savanna/Savanna.tsx @@ -0,0 +1,459 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Savanna.scss"; +import { useState, useEffect } from "react"; +import { + Button, + Badge, + Modal, + ButtonToolbar, + ButtonGroup, + ProgressBar } from "react-bootstrap"; +import { BsVolumeMute, BsFillVolumeUpFill, BsArrowRepeat } from "react-icons/bs"; +import { BiBell, BiBellOff, BiExit } from "react-icons/bi"; + +import FullScreenWrapper from "../../FullScreenWrapper/FullScreenWrapper"; +import { Redirect } from "react-router"; +import Preview from "../Preview/Preview"; +import useLocalStorage from "../../../hooks/useLocalStorage"; +import getUserData from "../../../api/getUserData"; +import setUserData from "../../../api/setUserData"; +import SavannahImg from "../../../assets/img/games/savannah.jpg"; +import { url } from "../../../api/defData"; + +type Statistics = { correctAnswers: number; wrongAnswers: number }; +type Settings = { sound: boolean; speak: boolean }; +type AllStatistics = { [index: number]: Statistics }; +type AllSettings = { [index: string]: Settings }; + +const PREVIEW_HEADING = "Саванна"; +const PREVIEW__DESCRIPTION = + "Слово спускается в саванну, предлагается 5 вариантов его перевода, правильный только один. Твоя задача выбрать правильный перевод слова раньше чем слово коснётся земли."; +const NUM_OF_ANSWERS = 4; +const TIMEOUT_TIME = 450; +const NUM_OF_ATTEMPTS = 20; +const dateNow = new Date().getDate(); +const defStatistics: Statistics = { correctAnswers: 0, wrongAnswers: 0 }; +const defAllStatistics: AllStatistics = { + [dateNow]: { correctAnswers: 0, wrongAnswers: 0 }, +}; + +const Savannah = () => { + const settingsLocal:string | null = localStorage.getItem('settings'); + const locSettings:AllSettings = settingsLocal ? JSON.parse(settingsLocal) : {savanna: {sound: true, speak: true}}; + const settings:Settings = locSettings.savanna; + const {sound, speak} = settings; + const questWordStyleDef = {top: "-2vw"}; + const defButtonsVariants = [ + "primary", + "primary", + "primary", + "primary", + "primary", + ]; + const defButtons = [ + "Отсутствуют", + "данные", + "с", + "сервера", + "!", + ]; + const [words, setWords] = useState(null); + const [wordsSet, setWordsSet] = useState(null); + // const [level, setLevel] = useState(null); //TODO: get level from book page + const level = null; + const [questWordStyle, setQuestWordStyle] = useState(questWordStyleDef); + const [soundOff, setSoundOff] = useState(sound); + const [isSpeak, setIsSpeak] = useState(speak); + const [timeoutTime, setTimeoutTime] = useState(TIMEOUT_TIME); + const [timerStart, setTimerStart] = useState(false); + const [showModal, setShowModal] = useState(false); + const [lives, setLives] = useState(100); + const [question, setQuestion] = useState(""); + const [questionId, setQuestionId] = useState(""); + const [answerTrue, setAnswerTrue] = useState(""); + const [answer, setAnswer] = useState(""); + const [attempt, setAttempt] = useState(0); + const [statistics, setStatistics] = useLocalStorage("savanna", defStatistics); + const [allStatistics, setAllStatistics] = useState(defAllStatistics); + const [buttonsVariants, setButtonsVariants] = useState(defButtonsVariants); + const [buttons, setButtons] = useState(defButtons); + const [wrongAnswersWords, setWrongAnswersWords] = useState>([]); + const [wrongWords, setWrongWords] = useState>([]); + const [exit, setExit] = useState(false); + const token = localStorage.getItem("token"); + const userId = localStorage.getItem("userId"); + + const handleClose = () => { + allStatisticsCompare(statistics); + setStatistics(defStatistics); + setWrongWords([]); + setShowModal(false); + setLives(100); + setAttempt(0); + setTimerStart(false); + }; + + const replay = () => { + setTimeoutTime(TIMEOUT_TIME); + setQuestWordStyle(questWordStyleDef); + setAnswer(''); + setWords(null); + setWrongWords([]); + setLives(100); + setAttempt(0); + setTimerStart(false); + }; + + const handleShow = () => setShowModal(true); + + const setUserWords = (words: any) => { + setWords(words); + getUserStatistics(); + }; + + const shuffleWords = (words: any) => { + if (words) { + let j, temp; + for (let i = words.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + temp = words[j]; + words[j] = words[i]; + words[i] = temp; + } + return words; + } + } + + useEffect(() => { + const settingsLocal:string | null = localStorage.getItem('settings'); + const locSettings:AllSettings = settingsLocal ? JSON.parse(settingsLocal) : {savanna: {sound: true, speak: true}}; + let settings:Settings = locSettings.savanna; + settings = {sound: soundOff, speak: isSpeak}; + locSettings.savanna = settings; + localStorage.setItem('settings', JSON.stringify(locSettings)) + + async function setUserSettings() { + if (settings && userId && token) { + const newSettings = { savanna: settings }; + const fullUrl = `${url}users/${JSON.parse(userId)}/settings`; + const bearerToken = JSON.parse(token); + await setUserData(fullUrl, bearerToken, newSettings) + .then((responseData: any) => {}) + .catch((error) => { + console.log(error.message); + }); + } + } + setUserSettings(); + }, [soundOff, isSpeak]); + + useEffect(() => { + const keyCodes = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5"]; + const handleKeyDown:any = (event:any) => { + if (keyCodes.indexOf(event.code) !== -1) setAnswer(buttons[keyCodes.indexOf(event.code)]) + }; + window.addEventListener('keyup', handleKeyDown); + return () => { + window.removeEventListener('keyup', handleKeyDown); + }; + }, [buttons]); + + useEffect(() => { + setWordsSet(shuffleWords(words)); + }, [words]); + + useEffect(() => { + if (wordsSet && !showModal && words) { + if (attempt < NUM_OF_ATTEMPTS) { + setButtonsVariants(defButtonsVariants); + const wordQuest = wordsSet[attempt]; + if (isSpeak) { + const audio = new Audio(url + wordQuest.audio); + audio.play(); + } + setQuestion(wordQuest.word); + setQuestionId(wordQuest.id); + setAnswerTrue(wordQuest.wordTranslate); + const randArr=[]; + const randWords=[wordQuest.wordTranslate] + for(let i = 0; i < NUM_OF_ANSWERS; i++) { + + const rand = Math.floor(Math.random() * wordsSet.length); + const randWord = wordsSet[rand]; + if (randArr.indexOf(rand) === -1 && rand !== attempt) { + randArr.push(rand); + randWords.push(randWord.wordTranslate); + } else i--; + setButtons(shuffleWords(randWords)); + } + } + }; + }, [wordsSet, attempt]); + + useEffect(() => { + if (question) + setTimerStart(true); + setTimeoutTime(TIMEOUT_TIME) + }, [question]); + + useEffect(() => { + if (timerStart && words) { + if (timeoutTime > 0) { + const questWordStyle = {top: `${45 - (timeoutTime / 10)}vw`}; + setQuestWordStyle(questWordStyle); + setTimeout(() => { + setTimeoutTime(timeoutTime - 1)}, 15); + } else { + setAnswer('wrong'); + setTimerStart(false); + setTimeoutTime(TIMEOUT_TIME); + }; + } + }, [timeoutTime, timerStart]); + + useEffect(() => { + if (answer) { + setTimerStart(false); + let answerSound = "correct.mp3"; + const trueAnswerIdx = buttons.indexOf(answerTrue); + buttonsVariants[trueAnswerIdx] = "success"; + if (answer !== answerTrue || answer === "wrong") { + const wrongAnswerIdx = buttons.indexOf(answer); + if (wrongAnswerIdx !== -1) buttonsVariants[wrongAnswerIdx] = "danger"; + answerSound = "error.mp3"; + const words: Array = wrongWords; + const wrongWord = [question, answerTrue, questionId]; + words.push(wrongWord); + statistics.wrongAnswers = statistics.wrongAnswers + 1; + setWrongWords(words); + setLives(lives - 20); + } else { + statistics.correctAnswers = statistics.correctAnswers + 1; + } + setButtonsVariants(buttonsVariants); + setStatistics(statistics); + setAnswer(""); + if (soundOff) { + const audio = new Audio(`${url}files/${answerSound}`); + audio.play(); + } + if (lives !== 0 || attempt !== NUM_OF_ATTEMPTS) setTimeout(() => { setAttempt(attempt + 1) }, 2000); + } + }, [answer]); + + useEffect(() => { + if (lives === 0 || attempt === NUM_OF_ATTEMPTS) { + const modalWrongWords: Array = []; + wrongWords.forEach((el: Array) => { + const word = ( +

+ {el[0]} - {el[1]} +

+ ); + if (wrongWords.indexOf(word) === -1) modalWrongWords.push(word); + }); + setWrongAnswersWords(modalWrongWords); + handleShow(); + }; + }, [lives, attempt, wrongWords]); + + async function getUserStatistics() { + if (token && userId) { + const fullUrl = `${url}users/${JSON.parse(userId)}/statistics`; + const bearerToken = JSON.parse(token); + await getUserData(fullUrl, bearerToken) + .then((responseData: any) => { + if (responseData.savanna) setAllStatistics(responseData.savanna); + }) + .catch((error) => { + console.log(error.message); + }); + } + } + + async function setUserStatistics() { + if (token && userId && allStatistics) { + const newStatistics = { savanna: allStatistics }; + const fullUrl = `${url}users/${JSON.parse(userId)}/statistics`; + const bearerToken = JSON.parse(token); + await setUserData(fullUrl, bearerToken, newStatistics) + .then((responseData: any) => {}) + .catch((error) => { + console.log(error.message); + }); + } + } + + const allStatisticsCompare = (statistics: Statistics) => { + if (!allStatistics[dateNow]) allStatistics[dateNow] = statistics; + else { + allStatistics[dateNow].correctAnswers = + allStatistics[dateNow].correctAnswers + statistics.correctAnswers; + allStatistics[dateNow].wrongAnswers = + allStatistics[dateNow].wrongAnswers + statistics.wrongAnswers; + } + setAllStatistics(allStatistics); + setUserStatistics(); + }; + + const buttonsBar = ( + + + + + + + + + + + + + + + ) + const progressBar = <>; + const attemptsBar = <>; + + const answersButtons = () => { + const buttonsArr: JSX.Element[] = []; + if (buttons) { + buttons.forEach((button:any, i) => { + buttonsArr.push( + ) + }); + } + return buttonsArr; + }; + const questionWord = ( +
+
+
+ + {question} + +
+
+ ); + + const gameWrapper = (
+
+
+ {answersButtons()} +
+
+
+ {questionWord} +
+
); + + + const Game = ( + <> +
+ {progressBar} +
+ {buttonsBar} + {attemptsBar} +
+
+ {gameWrapper} + + ); + + return ( +
+ + {exit && } + + + Игра окончена + + +
+

Ваш результат:

+
+

Верных ответов:

+ {statistics.correctAnswers} +

Неверных ответов:

+ {statistics.wrongAnswers} +
+

Необходимо повторить слова:

+
{wrongAnswersWords}
+
+
+ + + + + +
+ {words === null ? ( + + ) : ( +
+ {!wordsSet && "Набор слов отсутствует"} + {wordsSet && Game} +
+ )} +
+
+ ); +}; + +export default Savannah; diff --git a/rslang/src/components/Games/Speakit/Speakit.scss b/rslang/src/components/Games/Speakit/Speakit.scss new file mode 100644 index 000000000..abb590fdc --- /dev/null +++ b/rslang/src/components/Games/Speakit/Speakit.scss @@ -0,0 +1,167 @@ +.speak-it { + min-height: 70vh; + min-width: 100%; + + .speak-it-game { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + position: relative; + height: 100%; + width: 100%; + border-radius: 10px; + } + + .menu { + margin-bottom: 10px; + + .menu-wrapper { + display: flex; + justify-content: space-between; + + .rating{ + margin: 10px; + width: 200px; + height: 30px; + border-radius: 10px; + font-size: 1.5rem; + } + + .btns-toolbar{ + margin: 5px; + + .btn-group { + margin: 0 10px; + } + } + } + } + + .training-wrapper, + .game-wrapper { + display: block; + height: 85%; + width: 100%; + + .info-wrapper { + display: flex; + // height: 400px; + width: 100%; + align-items: center; + justify-content: center; + text-align: center; + margin: 0 auto 10px auto; + + .card-info:hover { + cursor: none; + } + + p { + font-size: 20px; + font-weight: 700; + width: fit-content; + height: fit-content; + vertical-align: middle; + background-color: #aeaeae; + border-radius: 20px; + padding: 8px 30px; + margin: 20px auto 0 auto; + } + .card-body { + cursor: default; + max-height: 450px; + max-width: 400px; + + .card-image-wrapper { + width: 355px; + height: 236px; + margin: 0 auto; + + .card-img-top { + max-height: 100%; + max-width: 100%; + } + } + + .text1 { + min-height: 45px; + } + + } + + .answer, + .true-answer { + display: flex; + justify-content: center; + align-items: center; + width: fit-content; + background-color: #ccc; + padding: 10px; + border-radius: 20px; + cursor: pointer; + font-size: 1.5rem; + font-weight: 500; + margin: 10px auto 0 auto; + } + + .true-answer { + background-color: #28a728; + } + } + + .training-button { + display: block; + width: 30rem; + margin: 10px auto; + } + + .words-wrapper{ + height: fit-content; + width: 100%; + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + + .toast:hover { + cursor: pointer; + } + + .toast { + font-size: 18px; + border-radius: 10px; + justify-content: space-between; + margin: 0 10px 0.5rem 10px; + } + + .toast-body { + display: flex; + text-align: center; + justify-content: space-between; + align-items: center; + + .toast-icons { + display: flex; + margin-right: auto; + } + } + + } + + .hotkey { + width: 25px; + height: 25px; + font-weight: 450; + background-color: #777; + border-radius: 5px; + margin-right: 10px; + padding: 3px; + font-size: 1.3rem; + } + + } +} + + + + diff --git a/rslang/src/components/Games/Speakit/Speakit.tsx b/rslang/src/components/Games/Speakit/Speakit.tsx new file mode 100644 index 000000000..dbd461dec --- /dev/null +++ b/rslang/src/components/Games/Speakit/Speakit.tsx @@ -0,0 +1,490 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Speakit.scss"; +import { useState, useEffect } from "react"; +import { + Button, + ButtonGroup, + ButtonToolbar, + Card, + Modal, + ProgressBar, + Toast } from "react-bootstrap"; +import { BsFillVolumeUpFill, BsArrowRepeat } from "react-icons/bs"; +import { BiBell, BiBellOff, BiExit, BiMicrophone, BiMicrophoneOff } from "react-icons/bi"; +import FullScreenWrapper from "../../FullScreenWrapper/FullScreenWrapper"; +import { Redirect } from 'react-router'; +import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition'; +import { playAudioWord } from "../../../utils/AudioWord"; +import Preview from "../Preview/Preview"; +import useLocalStorage from "../../../hooks/useLocalStorage"; +import getUserData from "../../../api/getUserData"; +import setUserData from "../../../api/setUserData"; +import SpeakitImg from "../../../assets/img/games/speakitImg.png"; +import { url } from "../../../api/defData"; + +type word = { + audio: string, + audioExample: string, + audioMeaning: string, + group: number, + id: string, + image: string, + page: number, + textExample: string, + textExampleTranslate: string, + textMeaning: string, + textMeaningTranslate: string, + transcription: string, + word: string, + wordTranslate: string, +} + +type card = { + word: string, + wordTranslate: string, + transcription: string, + audio: string, + image: string +} + +type Statistics = {correctAnswers: number, wrongAnswers: number}; +type AllStatistics = {[index:number]: Statistics}; + +const PREVIEW_HEADING = "Скажи это"; +const PREVIEW__DESCRIPTION = + "Нажмите на карточку со словом, чтобы увидеть его перевод и услышать звучание. Нажмите на кнопку 'Тренировка произношения' и произнесите слово в микрофон."; +const NUM_OF_ANSWERS = 10; +const dateNow = new Date().getDate(); +const defStatistics:Statistics = {correctAnswers: 0, wrongAnswers: 0}; +const defAllStatistics:AllStatistics = {[dateNow]: {correctAnswers: 0, wrongAnswers: 0}}; +const defActiveCard = { + word: "", + wordTranslate: "", + transcription: "", + audio: "", + image: "files/02_0628.jpg" +}; + +const Speakit = () => { + const [words, setWords] = useState(null); + const [wordsSet, setWordsSet] = useState([]); + // const [level, setLevel] = useState(null); //TODO: get level from book page + const level = null; //TODO: get level from book page + const [isSound, setSound] = useState(true); + const [isMic, setMic] = useState(true); + const [showModal, setShowModal] = useState(false); + const [isTraining, setTraining] = useState(false); + const [answer, setAnswer] = useState(''); + const [attempt, setAttempt] = useState(0); + const [statistics, setStatistics] = useLocalStorage("savanna", defStatistics); + const [allStatistics, setAllStatistics] = useState(defAllStatistics); + const [buttons, setButtons] = useState([]); + const [activeCard, setActiveCard] = useState(defActiveCard); + const [exit, setExit] = useState(false); + const { finalTranscript, listening, resetTranscript } = useSpeechRecognition(); + const token = localStorage.getItem("token"); + const userId = localStorage.getItem("userId"); + + + const keyCodes = ["Digit0", "Digit1", "Digit2", "Digit3", "Digit4", + "Digit5", "Digit6", "Digit7", "Digit8", "Digit9", + "KeyM", "NumpadEnter", "Space", "Backspace", "KeyQ", "KeyR" + ]; + + const handleKeyDown:any = (event:any) => { + if (keyCodes.indexOf(event.code) !== -1) { + if (event.code === 'KeyM') { + setAnswer(''); + SpeechRecognition.startListening({language: 'en-US'}) + } + + if (event.code === 'Backspace') { + setTraining(!isTraining); + } + + if (isTraining && event.code === 'KeyR') { + setAttempt(0); + } + + if (keyCodes.indexOf(event.code) < 10) { + const indexButton:word = buttons[keyCodes.indexOf(event.code)]; + setActiveCard(indexButton); + resetTranscript(); + playAudioWord(indexButton.audio); + setAnswer(''); + } + } + + }; + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [buttons, activeCard]); + + const handleShow = () => setShowModal(true); + + const handleClose = () => { + allStatisticsCompare(statistics); + setStatistics(defStatistics); + setAttempt(0); + setShowModal(false) + }; + + const setUserWords = (words: any) => { + words.splice(NUM_OF_ANSWERS); + setWords(words); + getUserStatistics(); + }; + + const shuffleWords = (words: any) => { + if(words) { + let j, temp; + for(let i = words.length - 1; i > 0; i--) { + j = Math.floor(Math.random()*(i + 1)); + temp = words[j]; + words[j] = words[i]; + words[i] = temp; + } + return words + } + } + + useEffect(() => { + setWordsSet(shuffleWords(words)); + }, [words]); + + useEffect(() => { + setButtons(wordsSet); + }, [wordsSet]) + + useEffect(() => { + if (isTraining) { + setAttempt(0); + setActiveCard(wordsSet[0]); + SpeechRecognition.startListening({language: 'en-US'}); + } else { + setActiveCard(defActiveCard); + setAttempt(0); + SpeechRecognition.stopListening(); + setAnswer('') + } + }, [isTraining]) + + useEffect(() => { + if (wordsSet) { + if (isTraining) { + setActiveCard(wordsSet[attempt]); + SpeechRecognition.startListening({ continuous: true, language: 'en-US' }); + } else { + resetTranscript(); + setActiveCard(defActiveCard); + SpeechRecognition.stopListening(); + } + } + }, [attempt]) + + useEffect(() =>{ + if (finalTranscript) setAnswer(finalTranscript.toLocaleLowerCase()); + },[finalTranscript]); + + useEffect(() => { + if (isTraining) { + resetTranscript(); + let answerSound = "files/correct.mp3"; + if(attempt === NUM_OF_ANSWERS - 1) { + setModal() + } + if (!activeCard.word.match(answer)) { + answerSound = "files/error.mp3"; + statistics.wrongAnswers = statistics.wrongAnswers + 1; + } else { + statistics.correctAnswers = statistics.correctAnswers + 1; + } + if (isSound) playAudioWord(answerSound); + setStatistics(statistics); + setTimeout(() => setAttempt(attempt+1), 1000); + } + }, [answer]) + + const setModal = () => { + SpeechRecognition.stopListening(); + handleShow(); + }; + + async function getUserStatistics() { + if(token && userId) { + const fullUrl = `${url}users/${JSON.parse(userId)}/statistics`; + const bearerToken = JSON.parse(token); + await getUserData(fullUrl, bearerToken).then(( responseData:any ) => { + if(responseData.speakit) setAllStatistics(responseData.speakit); + }).catch(error => { + console.log(error.message) + }) + } + } + + async function setUserStatistics() { + if(token && userId && allStatistics) { + const newStatistics = {speakit: allStatistics} + const fullUrl = `${url}users/${JSON.parse(userId)}/statistics`; + const bearerToken = JSON.parse(token); + await setUserData(fullUrl, bearerToken, newStatistics).then(( _responseData:any ) => { + }).catch(error => { + console.log(error.message) + }) + } + } + + const allStatisticsCompare = (statistics: Statistics) => { + if (!allStatistics[dateNow]) allStatistics[dateNow] = statistics; + else { + allStatistics[dateNow].correctAnswers = allStatistics[dateNow].correctAnswers + statistics.correctAnswers; + allStatistics[dateNow].wrongAnswers = allStatistics[dateNow].wrongAnswers + statistics.wrongAnswers; + } + setAllStatistics(allStatistics); + setUserStatistics(); + }; + + const modalRender = ( + + + Тренировка завершена + + +
+

Ваш результат:

+
+

Верных ответов:

+ {statistics.correctAnswers} +

Неверных ответов:

+ {statistics.wrongAnswers} +
+
+
+ + + + + +
+ ); + + const trainingButton = ( + + ); + + const buttonsBar = ( + + + + + + + + + + + + + + + {trainingButton} + + + ) + const progressBar = <> + ; + + const wordsButtons = () => { + const buttonsArr: JSX.Element[] = []; + if (!buttons) return []; + buttons.forEach((button:any, i) => { + buttonsArr.push( + { + resetTranscript(); + playAudioWord(button.audio); + setAnswer(''); + setActiveCard(button)}}> + +
+
{i}
+ +
+ {button.transcription} - + { button.word } +
+
+ ); + }); + return (buttonsArr); + }; + + const wordCard = activeCard ? ( +
+ + +
+ +
+ {!isTraining && + ( + {activeCard.wordTranslate} + + )} + {isTraining && + (<> + { playAudioWord(activeCard.audio) }} + > + {activeCard.word.toUpperCase()} + + + {activeCard.transcription} + + )} + {isMic && (
{ + setAnswer(''); + SpeechRecognition.startListening({language: 'en-US'}) + }}> +
M
+ + {answer && answer} +
)} +
+
+
+ ) : (<>); + + const nextWordButton = ( + <> + + ) + + const gameWrapper = (
+ {wordCard} +
+ {wordsButtons()} +
+
+ ); + + const speaking = ( +
+ +
+ ); + + const trainingeWrapper = ( + <> +
+ { wordCard } + { speaking } + { nextWordButton } +
+ + ); + + const Game = ( + <> +
+ {progressBar} +
+ {buttonsBar} +
+
+ {isTraining ? trainingeWrapper : gameWrapper} + + ) + + return ( +
+ + {exit && } + {modalRender} + {words === null ? ( + + ) : ( +
+ {!wordsSet && ('Набор слов отсутствует')} + {wordsSet && Game} +
+ )} +
+
+ ); +}; + +export default Speakit; + \ No newline at end of file diff --git a/rslang/src/components/Games/Sprint/Sprint.scss b/rslang/src/components/Games/Sprint/Sprint.scss new file mode 100644 index 000000000..2faf425b6 --- /dev/null +++ b/rslang/src/components/Games/Sprint/Sprint.scss @@ -0,0 +1,254 @@ +.sprint-game { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + min-height: 70vh; + display: flex; + align-items: center; + color: black; +} + +.area-game { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + background-image: url("../../../assets/img/Run.jpg"); + width: 100%; + min-width: 700px; + height: 100%; + display: flex; + position: relative; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: white; +} + +.score { + font-size: 25px; + margin-left: 45px; + font-weight: 500; + color: #d3b2ff; + text-shadow: 4px -2px 5px black; + margin-bottom: 25px; +} + +.bonus-score { + color: rgb(78, 180, 78); + text-shadow: 4px -2px 5px black; + opacity: 0; + transition: all 0.1s; +} + +.active-bonus-score { + opacity: 1; +} + +.wordTranslate { + font-size: 30px; + font-weight: 500; + margin-bottom: 25px; + color: white; + text-shadow: 4px -2px 5px black; +} + +.wrapBtn { + width: 280px; + display: flex; + justify-content: space-between; + button { + font-size: 16px; + color: white; + background-color: #007bff; + padding: 10px; + border-radius: 5px; + outline: none; + } +} + +.wrapCircles { + width: 150px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; + span { + display: block; + width: 25px; + height: 25px; + background-color: rgb(219, 217, 217); + border-radius: 50%; + } +} + +.circle { + background-color: #ff0000 !important; +} + +.sound-btn { + width: 30px; + height: 30px; + position: absolute; + cursor: pointer; + transition: all 0.2s; + top: 10px; + left: 10px; + background-size: 100%; + background-image: url('../../../assets/img/soundOn.jpg'); +} + +.sound-btn:hover { + transform: scale(1.1); +} + +.sound-off { + background-image: url('../../../assets/img/soundOff.jpg'); +} + +.statistic-sprint { + width: 90%; + height: 90%; + display: none; + flex-direction: column; + padding: 30px; + font-size: 16px; + position: absolute; + border-radius: 10px; + overflow-y: scroll; + background-color: rgb(227, 223, 231); + div { + font-size: 14px; + } + h4 { + margin: auto; + } +} + +.statistic-sprint-active { + display: flex; +} + +.statistic-sprint::-webkit-scrollbar { + width: 10px; + background-color: #f9f9fd; + } + + .statistic-sprint::-webkit-scrollbar-thumb { + border-radius: 10px; + background-color: #18aaaa; + } + + .statistic-sprint::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.2); + border-radius: 10px; + background-color: #f9f9fd; + } + + .wrap-btns-statistic { + display: flex; + position: sticky; + justify-content: space-between; + width: 300px; + margin: 0px 0px 0px auto; + bottom: 0px; + button { + font-size: 14px; + color: white; + background-color: #007bff; + padding: 10px; + border-radius: 5px; + outline: none; + transition: all 0.1s; + } + + button:hover { + transform: scale(1.03); + } + } + + .btn-close-statistic { + width: 20px; + height: 20px; + background-image: url("../../../assets/img/close.png"); + background-size: 100%; + display: block; + position: absolute; + top: 10px; + right: 10px; + transition: all 0.1s; + cursor: pointer; + } + + .btn-close-statistic:hover { + transform: scale(1.1); + } + + .base-timer { + position: relative; + width: 100px; + height: 100px; + margin-bottom: 20px; + } + + .base-timer__svg { + transform: scaleX(-1); + } + + .base-timer__circle { + fill: none; + stroke: none; + } + + .base-timer__path-elapsed { + stroke-width: 7px; + stroke: grey; + } + + .base-timer__path-remaining { + stroke-width: 7px; + stroke-linecap: round; + transform: rotate(90deg); + transform-origin: center; + transition: 1s linear all; + fill-rule: nonzero; + stroke: currentColor; + } + + .base-timer__path-remaining.green { + color: rgb(65, 184, 131); + } + + .base-timer__path-remaining.orange { + color: orange; + } + + .base-timer__path-remaining.red { + color: red; + } + + .base-timer__label { + position: absolute; + width: 100px; + height: 100px; + top: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 35px; + font-weight: 700; + color: white; + text-shadow: 4px -2px 5px black; + } + +@media screen and (max-width: 991px) { + .area-game { + min-width: 500px; + } +} + +@media screen and (max-width: 768px) { + .area-game { + min-width: 300px; + width: 400px; + } +} \ No newline at end of file diff --git a/rslang/src/components/Games/Sprint/SprintGame.tsx b/rslang/src/components/Games/Sprint/SprintGame.tsx new file mode 100644 index 000000000..8b21cf25e --- /dev/null +++ b/rslang/src/components/Games/Sprint/SprintGame.tsx @@ -0,0 +1,352 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Sprint.scss"; +import SprintImg from "../../../assets/img/Run.jpg"; +import React, { useState, useEffect } from "react"; +import { Route, Switch } from "react-router-dom"; +import { Link } from "react-router-dom"; +import { url } from "../../../api/defData"; +import FullScreenWrapper from "../../FullScreenWrapper/FullScreenWrapper"; +import Preview from "../Preview/Preview"; + +import getUserData from "../../../api/getUserData"; +import setUserData from "../../../api/setUserData"; + +const PREVIEW_HEADING = "Спринт"; +const PREVIEW__DESCRIPTION = + "На экране есть слово на английском и перевод. Вы должны определить правильный перевод или неправильный"; + +const FULL_DASH_ARRAY = 283; +const WARNING_THRESHOLD = 10; +const ALERT_THRESHOLD = 5; +const COLOR_CODES = { + info: { + color: "green" + }, + warning: { + color: "orange", + threshold: WARNING_THRESHOLD + }, + alert: { + color: "red", + threshold: ALERT_THRESHOLD + } +}; +let remainingPathColor = COLOR_CODES.info.color; + +const SprintGame = () => { + const [words, setWords] = useState(null); + const level = null; //TODO: get level from book page + const [count, setCount] = useState(60); + const [countStart, setCountStart] = useState(false); + const [word, changeWord] = useState(null); + const [trueTranslate, setTrueTranslate] = useState(); + const [wordTranslate, changeWordTranslate] = useState(null); + const [score, changeScore] = useState(0); + const [valueResponse, changeValueResponse] = useState(true); + const [indexWord, changeIndexWord] = useState(0); + const [countTrueAnswers, changeCountTrueAnswers] = useState(0); + const [countAllAnswers, changeCountAllAnswers] = useState(0); + const [bonusScore, changeBonusScore] = useState(1); + const [soundOn, changeSoundOn] = useState(true); + const [classSound, changeClassSound] = useState('sound-btn'); + const [classBonusScore, changeClassBonusScore] = useState('bonus-score'); + const [classStatistic, changeClassStatistic] = useState('statistic-sprint'); + const [arrWrongAnswer, changeArrWrongAnswer] = useState([]); + const [timerTimeOut, changeTimeOut] = useState(null) + + const [allStatistics, setAllStatistics] = useState(false); + const token = localStorage.getItem("token"); + const userId = localStorage.getItem("userId"); + + const [info, changeInfo] = useState('') + + const setUserWords = (words: any) => { + setWords(words); + getUserStatistics() + setCountStart(true); + changeIndexWord(0) + changeScore(0); + changeBonusScore(1); + changeCountTrueAnswers(0) + changeCountAllAnswers(0) + changeArrWrongAnswer([]); + changeClassStatistic('statistic-sprint'); + changeWordAndTranslate(words); + }; + + const checkEndGame = () => { + let compareStatistic = false; + if(indexWord === 21 || count === 0) { + changeClassStatistic('statistic-sprint statistic-sprint-active'); + if(count < 58){ + compareStatistic = true + }; + setCount(60) + } + if(compareStatistic){ + allStatisticsCompare(); + } + } + + const changeWordAndTranslate = (words: any) => { + checkEndGame(); + if(indexWord < 20){ + const randomResponse = Math.floor(Math.random() * 10) < 5 ? false : true; + changeValueResponse(randomResponse); + changeWord(words[indexWord].word); + setTrueTranslate(words[indexWord].wordTranslate); + if(randomResponse){ + changeWordTranslate(words[indexWord].wordTranslate); + } else { + let randomTranslate = null; + while(!!randomTranslate === false){ + const random = Math.floor(Math.random() * 20); + if(random !== indexWord){ + randomTranslate = random; + changeWordTranslate(words[randomTranslate].wordTranslate); + } + } + } + } + if(indexWord < 21 ) changeIndexWord(indexWord + 1); + } + + const checkAnswer = (value: boolean) => { + changeCountAllAnswers(countAllAnswers + 1); + let sound = null; + if(value === valueResponse && !!indexWord){ + sound = "correct.mp3"; + changeScore(score + 10 * bonusScore); + + if(countTrueAnswers < 3) { + changeCountTrueAnswers(countTrueAnswers + 1); + } else { + changeCountTrueAnswers(0); + changeBonusScore(bonusScore + 1); + } + changeClassBonusScore('bonus-score active-bonus-score'); + setTimeout(() => {changeClassBonusScore('bonus-score')}, 500); + + } else { + sound = "error.mp3"; + let newArr = arrWrongAnswer.slice(); + newArr.push([word, trueTranslate]); + changeArrWrongAnswer(newArr); + changeCountTrueAnswers(0); + changeBonusScore(1); + } + if(soundOn){ + const audio = new Audio(`${url}files/${sound}`); + audio.play(); + } + changeWordAndTranslate(words); + } + + const changeImgSound = () => { + const copySoundOn = !soundOn; + changeSoundOn(!soundOn); + changeClassSound(copySoundOn ? 'sound-btn' : 'sound-btn sound-off') + } + + const gameAgain = () => { + changeIndexWord(0) + changeScore(0); + changeBonusScore(1); + changeCountTrueAnswers(0) + changeArrWrongAnswer([]); + changeCountAllAnswers(0) + changeWordAndTranslate(words); + changeClassStatistic('statistic-sprint'); + } + + async function getUserStatistics() { + if (token && userId) { + const fullUrl = `${url}users/${JSON.parse(userId)}/statistics`; + const bearerToken = JSON.parse(token); + await getUserData(fullUrl, bearerToken) + .then((responseData: any) => { + if (responseData.sprint) setAllStatistics(responseData.sprint); + if (!responseData.sprint) setAllStatistics({}) + }) + .catch((error) => { + console.log(error.message); + }); + } + } + + async function setUserStatistics() { + if (token && userId && allStatistics) { + const newStatistics = { sprint: allStatistics }; + const fullUrl = `${url}users/${JSON.parse(userId)}/statistics`; + const bearerToken = JSON.parse(token); + await setUserData(fullUrl, bearerToken, newStatistics) + .then((responseData: any) => {}) + .catch((error) => { + console.log(error.message); + }); + } + } + + const allStatisticsCompare = () => { + const dateNow = new Date().getDate(); + const countCorrectAnswers = indexWord - arrWrongAnswer.length - 1; + const countWrongAnswers = arrWrongAnswer.length; + if (!allStatistics[dateNow]){ + allStatistics[dateNow] = {} + allStatistics[dateNow].correctAnswers = countCorrectAnswers; + allStatistics[dateNow].wrongAnswers = countWrongAnswers; + } else { + allStatistics[dateNow].correctAnswers = + allStatistics[dateNow].correctAnswers + countCorrectAnswers; + allStatistics[dateNow].wrongAnswers = + allStatistics[dateNow].wrongAnswers + countWrongAnswers; + } + + changeIndexWord(0) + setAllStatistics(allStatistics); + setUserStatistics(); + }; + + + useEffect(() => { + let timer=null + if(count !== 0 && countStart){ + timer = setTimeout(() => { + const newCount = count - 1; + setCount(newCount) + }, 1000); + } if(classStatistic === 'statistic-sprint statistic-sprint-active'){ + setCount(60) + clearTimeout(timerTimeOut) + } + checkEndGame(); + changeTimeOut(timer); + }, [count, countStart]) + + const hotKeys = (event: any) => { + if(classStatistic !== 'statistic-sprint statistic-sprint-active'){ + if(event.code === "Digit1"){ + checkAnswer(true); + } if(event.code === "Digit2"){ + checkAnswer(false); + } + } + } + + useEffect(() => { + window.addEventListener("keydown", hotKeys); + + return () => { + window.removeEventListener("keydown", hotKeys); + }; + }, [words, indexWord, allStatistics]); + + const blockCircles = (countTrueAnswers: number) => { + const arr = [0,0,0,0]; + const result = arr.map((elem, index) => { + if(index < countTrueAnswers){ + return (); + } else { + return (); + } + }); + return result; + }; + + const wrongAnswers = + arrWrongAnswer.map((elem, index) => { + return (

{elem[0]} - {elem[1]}

) + }); + + const timerAnimation = +
+ + + + + + + {count} +
+ + return ( +
+ + {words === null ? ( + + ) : ( +
+ { + changeImgSound(); + }} + > + {timerAnimation} +

+ Очки: {score} + +{10 * bonusScore} +

+
+ {blockCircles(countTrueAnswers)} +
+

{word} - {wordTranslate}

+
+ + +
+
+ + + +

Игра окончена

+

Ваш результат: {score}

+

Верных ответов - {countAllAnswers - arrWrongAnswer.length}

+

Неверных ответов - {arrWrongAnswer.length}

+

Необходимо повторить слова:

+
{wrongAnswers}
+
+ + + + + + +
+
+
+ )} +
+
+ ); +}; + +export default SprintGame; \ No newline at end of file diff --git a/rslang/src/components/HomePage/HomePage.scss b/rslang/src/components/HomePage/HomePage.scss new file mode 100644 index 000000000..a8e14f359 --- /dev/null +++ b/rslang/src/components/HomePage/HomePage.scss @@ -0,0 +1,111 @@ +.link-logo-block:hover { + text-decoration: none; +} +.logo { + font-weight: 900; + font-size: 2rem; + margin-top: 25px; + text-align: end; + padding-right: 20px; + font-family: "Chango", cursive; +} +.main-block { + min-height: 650px; +} +.main-img { + background: url(../../assets/img/main.jpg) no-repeat; + background-size: 100%; + margin-top: 50px; + margin-right: -60px; + float: right; + z-index: 100; + overflow: overlay; +} +.header { + margin-top: 25px; +} +.nav-link { + font-size: 1rem; + font-weight: 500; +} +.login-button { + font-weight: 300; + padding: 7px 27px; + font-weight: 400; +} +.description-block { + float: right; +} +.description-head { + text-align: end; + font-size: 1.5rem; + font-weight: 900; + p { + font-weight: 900; + font-size: 1.5rem; + text-align: end; + font-family: "Chango", cursive; + } +} +.description-text { + font-size: 1rem; + line-height: 20px; +} +.description-btn { + width: 150px; + height: 50px; +} +.teaching-head { + font-size: 1.5rem; + font-weight: 800; +} +.card { + transition: 1s; +} +.card:hover { + cursor: pointer; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} +.card-team-tilte { + text-align: end; +} +.card-title { + font-weight: 600; +} +.card-text { + text-align: center; +} +.team-card-profession-text { + text-align: end; +} + +.rss { + display: block; + position: relative; + // font-family: "Open Sans", sans-serif; + width: 86px; + height: 32px; + background-image: url("../../assets/svg/rss.svg"); + background-size: contain; + background-repeat: no-repeat; + background-position: left center; + padding-right: 111px; +} +.rss-year { + position: absolute; + bottom: 0; + right: 0; + font-size: 21px; + letter-spacing: -2px; + color: #343a40; + line-height: 0.9; + font-weight: bold; + transition: 0.3s; +} +.rss:hover .rss-year { + right: -5px; + letter-spacing: 0; +} +.footer-link-git { + font-weight: 500; +} diff --git a/rslang/src/components/HomePage/HomePage.tsx b/rslang/src/components/HomePage/HomePage.tsx new file mode 100644 index 000000000..d963e3c86 --- /dev/null +++ b/rslang/src/components/HomePage/HomePage.tsx @@ -0,0 +1,380 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./HomePage.scss"; +import React from "react"; +import { Container, Nav, Card } from "react-bootstrap"; +import { Link } from "react-router-dom"; +import Signin from "../Signin/SignIn"; +import Words from "../../assets/img/words.jpg"; +import Articles from "../../assets/img/articles.jpg"; +import Tests from "../../assets/img/tests.jpg"; +import Games from "../../assets/img/games.jpg"; +import Progress from "../../assets/img/progress.jpg"; +import BackgroundFeatures from "../../assets/img/background_features.jpg"; +import Video from "../../assets/img/video.jpg"; +import Ava_1 from "../../assets/img/ava_1.jpg"; +import Ava_2 from "../../assets/img/ava_2.jpg"; +import Ava_3 from "../../assets/img/ava_3.jpg"; +import Ava_4 from "../../assets/img/ava_4.jpg"; + +interface InterfaceHomePage {} + +const HomePage: React.FC = (props) => { + return ( + + + + +

RS Lang

+ +
+
+ + +
+ +
+
+

+ Изучайте Английский вместе с

RS Lang.

+

+

+ Ресурс позволяет учить английский онлайн при помощи метода + интервального повторения, отслеживания индивидуального прогресса и + мини-играми. +

+

+ Мы рекомендуем сделать упор именно на изучении новых слов. +

+

+ Метод интервального повторения. +

+

+ Первый раз вы должны повторить это слово где-нибудь через пару + минут, потом — через час, далее — на следующий день, затем — через + 2 дня, 5 дней, 10 дней, 3 недели, 6 недель, 3 месяца, 6 месяцев и + т.д. И вуаля: вы на всю жизнь запомните, что за словом «cat» + скрывается некто пушистый и мурлыкающий.{" "} +

+ +
+
+
+ + + +

Особенности приложения

+ Background Features +
+ + + + + + Слова и готовый фразы + + + Расширяй свой словарный запас и учи популярные фразы. Они + выручат в любой ситуации, помогут поддержать разговор, сделают + речь живой и непринужденной. + + + + + + + + Рассказы и статьи + + Если вы регулярно читаете художественные произведения + англоязычных писателей, вы осваиваете «правильный» английский + язык, который отличается от разговорного. + + + + + + + + Тесты + + Тесты благоприятно влияют на быстрое изучение нового материала и + закрепление уже пройденного, они могут заменить Вам учителя при + изучении английского самостоятельно. + + + + + + + Мини-игры + + Игры помогут вам не просто приятно провести время, но и + расширить словарный запас, подтянуть знание грамматики и + правописание. + + + + + + + + Статистика прогресса + + + Вне зависимости от того, играете ли вы или тренируете слова - + статистика по изученным словам обновляется и всегда доступна в + настройках. + + + + +
+ + +

Видео о работе приложения

+ + + Video + + +
+ + +

О команде

+ + +
+
+
+ ... +
+
+
+
+ {" "} + Aliaksei Savastsyanau +
+

+ Front-end Developer +

+

+ Разработал дизайн проекта. Реализовал "Стартовую + страницу", страницы:  "Электронного учебника",   + "Словаря",  "Статистики", "Загрузки" их функционал и  + адаптивность. +

+
+
+
+
+
+ +
+
+
+ ... +
+
+
+
+ Mitry Nayezzhy +
+

+ + Front-end, Back-end Developer, DevOps + +

+

+ Реализовал игры "Саванна" и "Скажи это", Авторизацию и + разавторизацию пользователя. Страницу настроек. + Расширил функционал Бэкэнда. Размещение и поддержка проекта. +

+
+
+
+
+
+ +
+
+
+ ... +
+
+
+
+ Anastasiya Ivanova{" "} +
+

+ Front-end Developer +

+

+ Реализовала мини-игру "Аудиовызов", блок выбора уровня + сложности, стартовый экран игры, результаты. +

+
+
+
+
+
+ +
+
+
+ ... +
+
+
+
Stas Tom
+

+ Front-end Developer +

+

+ Реализовал мини-игру "Спринт", экран выбора игры. +

+
+
+
+
+
+
+
+ + + + +
+ ); +}; +export default HomePage; diff --git a/rslang/src/components/LoadingScreen/LoadingScreen.scss b/rslang/src/components/LoadingScreen/LoadingScreen.scss new file mode 100644 index 000000000..79acab2c1 --- /dev/null +++ b/rslang/src/components/LoadingScreen/LoadingScreen.scss @@ -0,0 +1,91 @@ +.loader { + font-family: "Chango", cursive; + font-size: 3rem; + height: 20px; + width: 250px; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; +} +.loader--dot { + animation-name: loader; + animation-timing-function: ease-in-out; + animation-duration: 3s; + animation-iteration-count: infinite; + height: 20px; + width: 20px; + border-radius: 100%; + background-color: black; + position: absolute; + border: 2px solid white; +} +.loader--dot:first-child { + background-color: #8cc759; + animation-delay: 0.5s; +} +.loader--dot:nth-child(2) { + background-color: #8c6daf; + animation-delay: 0.4s; +} +.loader--dot:nth-child(3) { + background-color: #ef5d74; + animation-delay: 0.3s; +} +.loader--dot:nth-child(4) { + background-color: #f9a74b; + animation-delay: 0.2s; +} +.loader--dot:nth-child(5) { + background-color: #60beeb; + animation-delay: 0.1s; +} +.loader--dot:nth-child(6) { + background-color: #fbef5a; + animation-delay: 0s; +} +.loader--text { + position: absolute; + top: 200%; + left: 0; + right: 0; + margin: auto; +} +.loader--text:after { + content: "Loading"; + font-weight: bold; + animation-name: loading-text; + animation-duration: 3s; + animation-iteration-count: infinite; +} + +@keyframes loader { + 15% { + transform: translateX(0); + } + 45% { + transform: translateX(230px); + } + 65% { + transform: translateX(230px); + } + 95% { + transform: translateX(0); + } +} +@keyframes loading-text { + 0% { + content: "Loading"; + } + 25% { + content: "Loading."; + } + 50% { + content: "Loading.."; + } + 75% { + content: "Loading..."; + } +} diff --git a/rslang/src/components/LoadingScreen/LoadingScreen.tsx b/rslang/src/components/LoadingScreen/LoadingScreen.tsx new file mode 100644 index 000000000..1415125a4 --- /dev/null +++ b/rslang/src/components/LoadingScreen/LoadingScreen.tsx @@ -0,0 +1,25 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./LoadingScreen.scss"; +import React from "react"; +import { Container } from "react-bootstrap"; + +interface InterfaceLoadingScreen {} + +const LoadingScreen: React.FC = (props) => { + return ( + +
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; +export default LoadingScreen; diff --git a/rslang/src/components/Main/Main.scss b/rslang/src/components/Main/Main.scss new file mode 100644 index 000000000..e69de29bb diff --git a/rslang/src/components/Main/Main.tsx b/rslang/src/components/Main/Main.tsx new file mode 100644 index 000000000..b3a893e5b --- /dev/null +++ b/rslang/src/components/Main/Main.tsx @@ -0,0 +1,218 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Main.scss"; +import React, { useState, useEffect } from "react"; +import Study from "../Study/Study"; +import Vocabulary from "../Vocabulary/Vocabulary"; +import Games from "../Games/Games"; +import Statistics from "../Statistics/Statistics"; +import Settings from "../Settings/Settings"; +import { Route, Switch } from "react-router-dom"; +import useLocalStorage from "../../hooks/useLocalStorage"; +import getUserData from "../../api/getUserData"; +import setUserData from "../../api/setUserData"; +import getWords from "../../api/getWords"; +import { url } from "../../api/defData"; +import _ from "lodash"; +interface InterfaceMain {} + +const Main: React.FC = (props) => { + const [words, setWords] = useState([]); + const [allStatistics, setAllStatistics] = useLocalStorage( + "allStatistics", + "" + ); + const [learnedWords, setLearnedWords] = useLocalStorage("learnedWords", []); + const [hardWords, setHardWords] = useLocalStorage("hardWords", ""); + const [deletedWords, setDeletedWords] = useLocalStorage("deletedWords", ""); + const [correctAnswer, setCorrectAnswer] = useLocalStorage("correctAnswer", 0); + const [bestSeries, setBestSeries] = useLocalStorage("bestSeries", 0); + const [learnedWordToday, setLearnedWordToday] = useLocalStorage( + "learnedWordToday", + [] + ); + const [sortingDeletedWords, setSortingDeletedWords] = useLocalStorage( + "sortingDeletedWords", + "" + ); + + const [graphStatisticsDaily, setGraphStatisticsDaily] = useLocalStorage( + "graphStatisticsDaily", + [] + ); + const [ + graphStatisticsAllProgress, + setGraphStatisticsAllProgress, + ] = useLocalStorage("graphStatisticsAllProgress", []); + const Till = new Date().getDate(); + const [dateNow, setDateNow] = useLocalStorage("dateNow", 0); + + let [page, setPage] = useLocalStorage("page", 0); + const urlWords = `https://rocky-basin-33827.herokuapp.com/words?page=${page}&group=0`; + const token: any = localStorage.getItem("token"); + const userId: any = localStorage.getItem("userId"); + const tokenUse: any = JSON.parse(token); + const Id: any = JSON.parse(userId); + + useEffect(() => { + if (dateNow < Till) { + setGraphStatisticsDaily([ + ...graphStatisticsDaily, + learnedWordToday.length, + ]); + setGraphStatisticsAllProgress([ + ...graphStatisticsAllProgress, + learnedWords.length, + ]); + setLearnedWordToday([]); + + setDateNow(Till); + } + }, [Till]); + + useEffect(() => { + if (learnedWordToday.length / 20 === page + 1) { + setPage(++page); + } + }, [learnedWordToday]); + + async function setUserStatistics() { + if (tokenUse && Id) { + const newStatistics = { + vocabulary: { + learnedWords: learnedWords.length, + correctAnswer: correctAnswer, + bestSeries: bestSeries, + learnedWordToday: learnedWordToday.length, + graphStatisticsDaily: graphStatisticsDaily, + graphStatisticsAllProgress: graphStatisticsAllProgress, + }, + }; + const fullUrl = `${url}users/${Id}/statistics`; + const bearerToken = tokenUse; + await setUserData(fullUrl, bearerToken, newStatistics) + .then((responseData: any) => {}) + .catch((error) => { + console.log(error.message); + }); + } + } + + useEffect(() => { + setUserStatistics(); + }, [ + learnedWords, + hardWords, + deletedWords, + correctAnswer, + bestSeries, + sortingDeletedWords, + graphStatisticsDaily, + learnedWordToday, + graphStatisticsAllProgress, + ]); + + async function getStatistic(url: string, bearerToken: string) { + const fullUrl = `${url}users/${Id}/statistics`; + await getUserData(fullUrl, bearerToken) + .then((responseData: any) => { + setAllStatistics(responseData); + // console.log(responseData); + }) + .catch((error) => { + console.log(error.message); + }); + } + + useEffect(() => { + getStatistic(url, tokenUse); + }, [ + learnedWords, + hardWords, + deletedWords, + correctAnswer, + bestSeries, + sortingDeletedWords, + ]); + + async function getData(url: string, pref: string) { + const fullUrl = urlWords + pref; + const data: any = await getWords(fullUrl); + setWords(data); + } + + useEffect(() => { + getData(url, ""); + }, [page]); + + useEffect(() => { + setSortingDeletedWords( + _.differenceWith(learnedWords, deletedWords, _.isEqual) + ); + }, [learnedWords, deletedWords]); + + const getHardWords = (arr: any) => { + setHardWords(_.uniqWith(hardWords.concat(arr), _.isEqual)); + }; + + const getLearnedWords = (arr: any) => { + setLearnedWords(_.uniqWith(learnedWords.concat(arr), _.isEqual)); + }; + + const getLearnedWordToday = (arr: any) => { + setLearnedWordToday(_.uniqWith(learnedWordToday.concat(arr), _.isEqual)); + }; + + const getDeletedWords = (arr: any) => { + setDeletedWords(_.uniqWith(deletedWords.concat(arr), _.isEqual)); + }; + const getCorrectAnswer = (arr: any) => { + setCorrectAnswer(arr); + }; + const getBestSeries = (arr: any) => { + if (arr > bestSeries) { + setBestSeries(arr); + } + }; + + return ( + + + + + + + + + + + + + + + + + + ); +}; +export default Main; diff --git a/rslang/src/components/Menu/Menu.scss b/rslang/src/components/Menu/Menu.scss new file mode 100644 index 000000000..513985b3f --- /dev/null +++ b/rslang/src/components/Menu/Menu.scss @@ -0,0 +1,13 @@ +.logo { + font-weight: 900; + font-size: 1.5rem; + text-align: end; + padding-right: 20px; + font-family: "Chango", cursive; +} +.menu-elem { + font-weight: 600; +} +.nav-tabs .nav-link.active { + background-color: #f8f9fa; +} diff --git a/rslang/src/components/Menu/Menu.tsx b/rslang/src/components/Menu/Menu.tsx new file mode 100644 index 000000000..4b68358b2 --- /dev/null +++ b/rslang/src/components/Menu/Menu.tsx @@ -0,0 +1,66 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Menu.scss"; +import React from "react"; +import { Container, Nav } from "react-bootstrap"; +import { Link, withRouter } from "react-router-dom"; +interface InterfaceMenu {} + +const Menu: React.FC = (props) => { + return ( + + + + ); +}; +export default withRouter(Menu); diff --git a/rslang/src/components/Settings/Settings.scss b/rslang/src/components/Settings/Settings.scss new file mode 100644 index 000000000..b0e44aff0 --- /dev/null +++ b/rslang/src/components/Settings/Settings.scss @@ -0,0 +1,69 @@ +.user-settings-wrapper { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + flex-direction: column; + + .user-info { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + + .username { + font-size: 2em; + } + + .userpic { + width: 150px; + height: auto; + border-radius: 30%; + margin: 0 20px; + } + } + + .words-in-day-wrapper { + display: flex; + justify-content: center; + align-items: center; + + .words-in-day { + font-size: 1.6rem; + font-weight: 600; + } + + .words-per-day { + text-align: center; + width: 2rem; + font-size: 1.6rem; + font-weight: 600; + } + + } + + .settings-buttons { + margin: 5px 5px; + } + + + .settings-block { + h4 { + text-align: center; + } + display: flex; + flex-direction: column; + font-size: 1.3rem; + margin: 10px 0; + + } + + .setting { + font-size: 1.3rem; + margin: 10px 0; + } +} + +.settings-button { + margin: 5px auto; +} \ No newline at end of file diff --git a/rslang/src/components/Settings/Settings.tsx b/rslang/src/components/Settings/Settings.tsx new file mode 100644 index 000000000..484199a61 --- /dev/null +++ b/rslang/src/components/Settings/Settings.tsx @@ -0,0 +1,162 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Settings.scss"; +import { useState, useEffect } from "react"; +import { Container } from "react-bootstrap"; +import setUserData from "../../api/setUserData"; +import { Button, Form } from "react-bootstrap"; +import { BsVolumeMute, BsFillVolumeUpFill } from "react-icons/bs"; +import { BiBell, BiBellOff } from "react-icons/bi"; +import useLocalStorage from "../../hooks/useLocalStorage"; +import { url } from "../../api/defData"; + +interface InterfaceSettings {} +interface dataInterface { + id: string, + wordsPerDay: number +} + +const Settings: React.FC = (props) => { + const settings = localStorage.getItem("settings"); + const parseSettings = settings ? JSON.parse(settings) : {}; + let {savanna, speakit, vocabulary, wordsPerDay} = parseSettings; + const soundSavannaLoc = savanna.sound; + const speakSavannaLoc = savanna.speak; + const soundSpeakItLoc = speakit.sound; + vocabulary = vocabulary; + const translateLoc = vocabulary ? vocabulary.translate : true; + const strongLoc = vocabulary ? vocabulary.strong : true; + const deletedLoc = vocabulary ? vocabulary.deleted : true; + const userId = localStorage.getItem("userId"); + const token = localStorage.getItem("token"); + let username = localStorage.getItem("username"); + username = username ? JSON.parse(username) : ""; + const userpic = localStorage.getItem("userpic"); + const [wordsPerDayNew, setWordsPerDay] = useState(wordsPerDay); + const [speakSavanna, setSpeakSavanna] = useState(speakSavannaLoc); + const [soundSavanna, setSoundSavanna] = useState(soundSavannaLoc); + const [soundSpeakIt, setSoundSpeakIt] = useState(soundSpeakItLoc); + const [translate, setTranslate] = useState(translateLoc); + const [deleted, setDeleted] = useState(deletedLoc); + const [strong, setStrong] = useState(strongLoc); + const [changed, setChanged] = useState(false); + + const avatarUrl = userpic ? `${url}${JSON.parse(userpic)}` : ''; + + const verWordsPerDay = (number: number) => { + if (number > 4 && number < 51) setWordsPerDay(number); + if (number < 4) setWordsPerDay(4); + if (number > 50) setWordsPerDay(50); + } + + useEffect(() => { + if (wordsPerDayNew !== wordsPerDay || + deleted !== deletedLoc || + strong !== strongLoc || + translate !== translateLoc || + speakSavanna !== speakSavannaLoc || + soundSavanna !== soundSavannaLoc || + soundSpeakIt !== soundSpeakItLoc) setChanged(true); + }, [wordsPerDayNew, + deleted, + strong, + translate, + speakSavanna, + soundSavanna, + soundSpeakIt]) + + async function setUserSettings(newSettings:object) { + if (settings && userId && token) { + const fullUrl = `${url}users/${JSON.parse(userId)}/settings`; + const bearerToken = JSON.parse(token); + await setUserData(fullUrl, bearerToken, newSettings) + .then((responseData: any) => {if (responseData) setChanged(false)}) + .catch((error) => { + console.log(error.message); + }); + } + }; + + const newSettingsSave = () => { + const newSettings = {wordsPerDay: wordsPerDayNew, + vocabulary: {strong: strong, deleted: deleted, translate}, + savanna: {sound: soundSavanna, speak: speakSavanna}, + speakit: {sound: soundSpeakIt}, + }; + newSettings.wordsPerDay = wordsPerDayNew; + localStorage.setItem("settings", JSON.stringify(newSettings)); + setUserSettings(newSettings); + }; + + return ( + + +

Настройки

+
+ +
+

{username}

+ {avatarUrl && user avatar} +
+
+ Я хочу учить + + {wordsPerDayNew} + + слов в день +
+
+

Настройки раздела Изучение

+
+
+ + {setTranslate(!translate)}} type="checkbox"/> + Отображать перевод изучаемого слова + + + {setStrong(!strong)}} type="checkbox"/> + Отображать кнопку "В сложные слова" + + + {setDeleted(!deleted)}} type="checkbox"/> + Отображать кнопку "В удалённые слова" + +
+
+
+
+

Настройки игры Саванна

+ + +
+
+

Настройки игры Скажи это

+ +
+
+
+
+ ); +}; + +export default Settings; \ No newline at end of file diff --git a/rslang/src/components/Signin/Login.tsx b/rslang/src/components/Signin/Login.tsx new file mode 100644 index 000000000..6bb21e0ee --- /dev/null +++ b/rslang/src/components/Signin/Login.tsx @@ -0,0 +1,277 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./signin.scss"; +import { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { Button, Form, Modal } from "react-bootstrap"; +import getUserData from "../../api/getUserData"; +import setUserData from "../../api/setUserData"; +import { Redirect } from "react-router"; +import { url, defSettingsData, defStatisticsData } from "../../api/defData"; + +const Login = () => { + const { register, handleSubmit, errors } = useForm(); + const [isLoged, setLoged] = useState(false); + const [needLogin, setNeedLogin] = useState(false); + const [message, setMessage] = useState(null); + + const localToken = localStorage.getItem("token"); + const localUserId = localStorage.getItem("userId"); + let token = localToken ? JSON.parse(localToken) : ""; + let userId = localUserId ? JSON.parse(localUserId) : ""; + + useEffect(() => { + if (token) { + const fullUrl = `${url}users/${userId}/statistics`; + getUserData(fullUrl, token) + .then((responseData: any) => { + localStorage.setItem("statistics", JSON.stringify(responseData)); + setTimeout(() => { + setLoged(true); + }, 1000); + }) + .catch((error) => { + console.log(error.message); + setMessage({ + data: "Токен устарел, введите данные для входа", + type: "", + }); + setTimeout(() => { + setNeedLogin(true); + }, 1000); + }); + } else { + setTimeout(() => { + setNeedLogin(true); + }, 1000); + } + }, [token, userId]); + + async function api(url: string, data: any): Promise { + const init: RequestInit = { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }; + const response = await fetch(url, init); + + if (!response.ok) { + const error = response.status + " " + response.statusText; + throw new Error(error); + } + const body = await response.json(); + return body; + } + + const getStatistics = () => { + const fullUrl = `${url}users/${userId}/statistics`; + getUserData(fullUrl, token) + .then((responseData: any) => { + localStorage.setItem("statistics", JSON.stringify(responseData)); + }) + .catch((error) => { + setMessage({ + data: "Создание новых данных", + type: "", + }); + const dateNow = new Date(); + // .toLocaleString("ru-Ru", { + // year: "numeric", + // month: "numeric", + // day: "numeric", + // }); + defStatisticsData.optional = { regDate: dateNow }; + setUserData(fullUrl, token, defStatisticsData) + .then((responseData: any) => { + localStorage.setItem("statistics", JSON.stringify(responseData)); + }) + .catch((error) => { + console.log(error.message); + }); + }); + }; + + const getSettings = () => { + if (userId && token) { + const fullUrl = `${url}users/${userId}/settings`; + getUserData(fullUrl, token) + .then((responseData: any) => { + localStorage.setItem("settings", JSON.stringify(responseData)); + }) + .catch((error) => { + setMessage({ data: "Создание новых данных", type: "" }); + setUserData(fullUrl, token, defSettingsData) + .then((responseData: any) => { + localStorage.setItem("settings", JSON.stringify(responseData)); + }) + .catch((error) => { + console.log(error.message); + }); + }); + } + }; + + const onSubmit = async (data: any): Promise => { + setMessage({ + data: `Выполняется вход...`, + type: "none", + }); + + const fullUrl = url + "signin"; + + api(fullUrl, data) + .then((responseData: any) => { + setMessage({ + data: "Вход выполнен", + type: "", + }); + token = responseData.token; + userId = responseData.userId; + setTimeout(setMessage, 4000); + localStorage.setItem( + "refreshToken", + JSON.stringify(responseData.refreshToken) + ); + localStorage.setItem("username", JSON.stringify(responseData.username)); + localStorage.setItem("userpic", JSON.stringify(responseData.userpic)); + localStorage.setItem("token", JSON.stringify(token)); + localStorage.setItem("userId", JSON.stringify(userId)); + setMessage({ + data: "Получение данных пользователя", + type: "", + }); + getSettings(); + getStatistics(); + setTimeout(() => setLoged(true), 5000); + }) + .catch((error) => { + console.log(error.message); + if (error.message === "403 Forbidden") { + setMessage({ + data: "Неверный пароль", + type: "alert-warning", + }); + } else if (error.message === "404 Not Found") { + setMessage({ + data: "Пользователь с таким email не найден", + type: "alert-warning", + }); + } else if (error.message) { + setMessage({ + data: "Ошибка входа", + type: "alert-warning", + }); + } + setTimeout(setMessage, 5000); + }); + }; + + return ( + <> + + {isLoged && } + {needLogin && ( +
+
+ {message && ( +
+ {message.data} + {message.type === "alert-warning" && ( + + )} +
+ )} +
+ + Email адрес + + + {errors.email && ( + + {errors.email.message} + + )} + + + + Пароль + + + {errors.password && ( + + {errors.password.message} + + )} + + +
+ )} +
+ + + + + ); +}; + +export default Login; diff --git a/rslang/src/components/Signin/Register.tsx b/rslang/src/components/Signin/Register.tsx new file mode 100644 index 000000000..af2ba069b --- /dev/null +++ b/rslang/src/components/Signin/Register.tsx @@ -0,0 +1,235 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./signin.scss"; +import React, { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Button, Form, Modal } from "react-bootstrap"; +import { url } from "../../api/defData"; + +const custom = 'custom'; +const defAvatar = 'avatars/defavatar.png' + +const Register = () => { + const { register, handleSubmit, errors } = useForm(); + + const [message, setMessage] = useState(null); + + async function api(url: string, userData: any): Promise { + console.log(userData.file.length) + if(userData.file.length === 0) {userData.userpic = defAvatar} + else {userData.userpic = custom} + const init: RequestInit = { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(userData) + }; + const response = await fetch(url, init); + + if (!response.ok) { + const error = response.status + " " + response.statusText; + throw new Error(error) + } + const body = await response.json(); + return body + } + + async function avatar(file: any, userId: string): Promise { + const fullUrl = `${url}avatar`; + const formData:any = new FormData(); + const newFileName = userId + '.jpg'; + formData.append("file", file[0], newFileName); + const init: RequestInit = { + method: 'POST', + headers: { + 'Accept': 'application/json' + }, + body: formData + }; + const response = await fetch(fullUrl, init); + console.log(response) + if (!response.ok) { + const error = response.status + " " + response.statusText; + throw new Error(error) + } + + const body = await response.json(); + + return body + } + + const onSubmit = async (userData: any): Promise => { + setMessage({ + data: `Выполняется регистрация...`, + type: "none", + }); + + const fullUrl = `${url}users`; + + api(fullUrl, userData).then(( responseData:any ) => { + setMessage({ + data: "Регистрация выполнена", + type: "", + }); + setTimeout(setMessage, 5000) + if (userData.userpic === custom) { + avatar(userData.file, responseData.id).then(( res:any ) => { + }).catch(error => { + console.log(error) + }) + } + }) + .catch(error => { + console.log(error.message) + if (error.message === "417 Expectation Failed") { + setMessage({ + data: "Такой email уже зарегистрирован", + type: "alert-warning", + }); + } else { + setMessage({ + data: "Ошибка регистрации", + type: "alert-warning", + }); + } + setTimeout(setMessage, 5000) + }) + }; + + return ( + <> + +
+
+ {message && ( +
{message.data} + { message.type === 'alert-warning' && ( )} +
)} +
+ + Email адрес + + + {errors.email && ( + + {errors.email.message} + + )} + + + + Имя + + + {errors.name && ( + + {errors.name.message} + + )} + + + + Пароль + + + {errors.password && ( + + {errors.password.message} + + )} + + + + + + Если файл не выбран или имеет тип отличный от *.jpg, *.png + + + будет установлено изображение по-умолчанию + + +
+
+ + + + + ); +}; +export default Register; diff --git a/rslang/src/components/Signin/SignIn.tsx b/rslang/src/components/Signin/SignIn.tsx new file mode 100644 index 000000000..a530a97ea --- /dev/null +++ b/rslang/src/components/Signin/SignIn.tsx @@ -0,0 +1,48 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./signin.scss"; +import React, { useState } from "react"; +import { Button, Modal, Tabs, Tab } from "react-bootstrap"; +import Register from "./Register"; +import Login from "./Login"; +interface SigninProps { + tab: string +} + +const Signin = (Props:SigninProps) => { + const [key, setKey] = useState(Props.tab); + const [show, setShow] = useState(false); + const handleClose = () => setShow(false); + const handleShow = () => setShow(true); + + return ( + <> + + + + Войдите или Зарегистрируйтесь + + setKey(k)}> + + + + + + + + + + + + + ); +}; +export default Signin; diff --git a/rslang/src/components/Signin/SignOut.tsx b/rslang/src/components/Signin/SignOut.tsx new file mode 100644 index 000000000..d555a106a --- /dev/null +++ b/rslang/src/components/Signin/SignOut.tsx @@ -0,0 +1,35 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./signin.scss"; +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; +import { Redirect } from 'react-router'; + + +interface SigninProps { +} + + + +const SignOut = (Props:SigninProps) => { + const [isLoged, setLoged] = useState(true); + + const handleSignOut = () => { + localStorage.removeItem('token'); + localStorage.removeItem('userpic'); + localStorage.removeItem('userId'); + localStorage.removeItem('refreshToken'); + setLoged(false) + }; + + return ( + <> + {!isLoged && } + + + ); +}; +export default SignOut; diff --git a/rslang/src/components/Signin/signin.scss b/rslang/src/components/Signin/signin.scss new file mode 100644 index 000000000..e02be23c8 --- /dev/null +++ b/rslang/src/components/Signin/signin.scss @@ -0,0 +1,19 @@ +.modal-dialog { + + .modal-header { + border-bottom: none; + } + + .modal-footer { + border-top: none; + padding: 0 0.75rem; + } + + .message { + height: 60px; + } + + .alert { + line-height: 25px; + } +} \ No newline at end of file diff --git a/rslang/src/components/Statistics/Statistics.scss b/rslang/src/components/Statistics/Statistics.scss new file mode 100644 index 000000000..7f4ce53d9 --- /dev/null +++ b/rslang/src/components/Statistics/Statistics.scss @@ -0,0 +1,6 @@ +.statistics-page-head-block { +} +.statistics-page-head { + font-size: 2rem; + font-weight: 900; +} diff --git a/rslang/src/components/Statistics/Statistics.tsx b/rslang/src/components/Statistics/Statistics.tsx new file mode 100644 index 000000000..88c81115f --- /dev/null +++ b/rslang/src/components/Statistics/Statistics.tsx @@ -0,0 +1,261 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Statistics.scss"; +import React, { useState, useEffect } from "react"; +import { Container, Table } from "react-bootstrap"; +import useLocalStorage from "../../hooks/useLocalStorage"; +import { url } from "../../api/defData"; +import { Bar, Line } from "react-chartjs-2"; + +interface InterfaceStatistics { + allStatistics: any; +} + +type Statistics = { correctAnswers: number; wrongAnswers: number }; +type AllStatistics = { [index: number]: Statistics }; + +const Statistics: React.FC = (props) => { + const [username, setUserName] = useLocalStorage("username", ""); + const [statisticUser, setStatisticUser] = useState(props.allStatistics); + const [barData, setBarData] = useState({}); + const [lineData, setLineData] = useState({}); + const [arrWordsLearnedToday, setArrWordsLearnedToday] = useLocalStorage( + "arrWordsLearnedToday", + [] + ); + const [arrWordsLearnedAll, setArrWordsLearnedAll] = useLocalStorage( + "arrWordsLearnedAll", + [] + ); + const [learnedWords, setLearnedWords] = useLocalStorage( + "learnedWordsStatistic", + 0 + ); + const [bestSeries, setBestSeries] = useLocalStorage("bestSeriesStatistic", 0); + const [correctAnswer, setCorrectAnswer] = useLocalStorage( + "correctAnswerStatistic", + 0 + ); + + const ARRAY_OF_DATES: any = []; + const D = new Date(props.allStatistics.optional.regDate); + const Till = new Date(); + + function pad(s: any) { + return `00${s}`.slice(-2); + } + + useEffect(() => { + while (D.getTime() < Till.getTime()) { + ARRAY_OF_DATES.push( + `${D.getFullYear()}-${pad(D.getMonth() + 1)}-${pad(D.getDate())}` + ); + D.setDate(D.getDate() + 1); + } + ARRAY_OF_DATES.push(Till); + }, []); + + useEffect(() => { + setLearnedWords(props.allStatistics.vocabulary.learnedWords); + setBestSeries(props.allStatistics.vocabulary.bestSeries); + setCorrectAnswer(props.allStatistics.vocabulary.correctAnswer); + setArrWordsLearnedToday( + props.allStatistics.vocabulary.graphStatisticsDaily + ); + setArrWordsLearnedAll( + props.allStatistics.vocabulary.graphStatisticsAllProgress + ); + }, [statisticUser]); + + useEffect(() => { + if (props.allStatistics.vocabulary.graphStatisticsDaily.length === 0) { + arrWordsLearnedToday[0] = statisticUser.vocabulary.learnedWordToday; + } else { + arrWordsLearnedToday[arrWordsLearnedToday.length] = + statisticUser.vocabulary.learnedWordToday; + } + }, [arrWordsLearnedToday]); + + useEffect(() => { + if ( + props.allStatistics.vocabulary.graphStatisticsAllProgress.length === 0 + ) { + arrWordsLearnedAll[0] = statisticUser.vocabulary.learnedWords; + } else { + arrWordsLearnedAll[arrWordsLearnedAll.length] = + statisticUser.vocabulary.learnedWords; + } + }, [arrWordsLearnedAll]); + + useEffect(() => { + setLineData({ + labels: ARRAY_OF_DATES, + datasets: [ + { + label: "Выучено слов", + data: arrWordsLearnedAll, + backgroundColor: ["rgba(54, 162, 235, 0.6)"], + borderWidth: 5, + }, + ], + }); + }, [statisticUser]); + + useEffect(() => { + setBarData({ + labels: ARRAY_OF_DATES, + datasets: [ + { + label: "Ежедневный прогресс", + data: arrWordsLearnedToday, + backgroundColor: [ + "rgba(255, 99, 132, 0.6)", + "rgba(54, 162, 235, 0.6)", + "rgba(255, 206, 86, 0.6)", + "rgba(75, 192, 192, 0.6)", + "rgba(255, 99, 13, 0.6)", + "rgba(54, 162, 35, 0.6)", + "rgba(255, 206, 6, 0.6)", + "rgba(75, 12, 92, 0.6)", + "rgba(255, 9, 132, 0.6)", + "rgba(54, 162, 235, 0.6)", + "rgba(955, 206, 6, 0.6)", + "rgba(75, 192, 12, 0.6)", + "rgba(275, 129, 192, 0.6)", + "rgba(54, 12, 235, 0.6)", + "rgba(755, 6, 1, 0.6)", + "rgba(75, 2, 12, 0.6)", + ], + borderWidth: 3, + }, + ], + }); + }, [statisticUser]); + + return ( + + +

Статистика

+
+ +

{"Статистика пользователя - " + username}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#{"Статистика пользователя - " + username}Результат
1Пройдено карточек за все время{learnedWords}
2Лучшая серия{bestSeries}
3Процент правильных ответов{correctAnswer} %
4Изучено новых слов{statisticUser.vocabulary.learnedWordToday}
+
+ + + + +
+ ); +}; +export default Statistics; diff --git a/rslang/src/components/Study/Study.scss b/rslang/src/components/Study/Study.scss new file mode 100644 index 000000000..101086ebe --- /dev/null +++ b/rslang/src/components/Study/Study.scss @@ -0,0 +1,19 @@ +.study-page-head-block { +} +.study-page-head { + font-size: 2rem; + font-weight: 900; +} +.study-page-head-text { + font-size: 1.2rem; + font-weight: 500; + line-height: 2rem; + text-align: center; +} +.card { + transition: 1s; +} +.card:hover { + cursor: pointer; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} diff --git a/rslang/src/components/Study/Study.tsx b/rslang/src/components/Study/Study.tsx new file mode 100644 index 000000000..c85c1faf2 --- /dev/null +++ b/rslang/src/components/Study/Study.tsx @@ -0,0 +1,191 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Study.scss"; +import React, { useState, useEffect } from "react"; +import { Container, Card, Button } from "react-bootstrap"; +import NewWords from "../../assets/img/new_words.jpg"; +import RepeatWords from "../../assets/img/repeat_words.jpg"; +import HardWords from "../../assets/img/hard_words.jpg"; +import NewWordsSection from "./StudySections/NewWordsSection"; +import RepeatWordsSection from "./StudySections/RepeatWordsSection"; +import HardWordsSection from "./StudySections/HardWordsSection"; +import _ from "lodash"; + +interface InterfaceStudy { + words: any; + hardWords: any; + learnedWords: any; + deletedWords: any; + learnedWordToday: any; + page: number; + getHardWords(arr: any): void; + getLearnedWords(arr: any): void; + getDeletedWords(arr: any): void; + getCorrectAnswer(arr: any): void; + getBestSeries(arr: any): void; + getLearnedWordToday(arr: any): void; +} + +const Study: React.FC = (props) => { + const [larnNewWord, setlarnNewWord] = useState(""); + const [hardWords, setHardWords] = useState([]); + const [learnedWords, setLearnedWords] = useState([]); + const [deletedWords, setDeletedWords] = useState([]); + const [learnedWordToday, setLearnedWordToday] = useState([]); + + useEffect(() => { + props.getHardWords(hardWords); + }, [hardWords]); + + useEffect(() => { + props.getLearnedWords(learnedWords); + }, [learnedWords]); + + useEffect(() => { + props.getDeletedWords(deletedWords); + }, [deletedWords]); + + useEffect(() => { + props.getLearnedWordToday(learnedWordToday); + }, [learnedWordToday]); + + const getHardWords = (arr: any) => { + setHardWords(_.uniqWith(hardWords.concat(arr), _.isEqual)); + }; + const getLearnedWords = (arr: any) => { + setLearnedWords(_.uniqWith(learnedWords.concat(arr), _.isEqual)); + }; + const getDeletedWords = (arr: any) => { + setDeletedWords(_.uniqWith(deletedWords.concat(arr), _.isEqual)); + }; + const getLearnedWordToday = (arr: any) => { + setLearnedWordToday(_.uniqWith(learnedWordToday.concat(arr), _.isEqual)); + }; + + const startSectionWithWords = (section: string) => { + setlarnNewWord(section); + }; + + const closePage = (str: string) => { + setlarnNewWord(str); + }; + + const showPageStudy = () => { + return ( + + +

Привет.

+

+ Приступим к обучению! +

+
+

+ На этой странице вы можете следить за своим прогрессом и выбирать + желаемый набор слов для изучения, например,{" "} + “Новые слова” , “Повторить слова” + или “Сложные слова” . Удачи! +

+ +

Сегодня изучено

+

+ Сегодня изучено: {props.learnedWordToday.length} из{" "} + {props.words.length * (props.page + 1)} слов +

+
+ + + + + Новые слова + + Нажмите, чтобы выучить новые слова на сегодня. + + + + + + + + Повторить слова + Нажмите, чтобы повторить выученные слова. + + + + + + + + Сложные слова + Нажмите, чтобы повторить сложные слова. + + + + +
+ ); + }; + + if (larnNewWord === "NewWordsSection") { + return ( + + ); + } else if (larnNewWord === "HardWordsSection") { + return ( + + ); + } else if (larnNewWord === "RepeatWordsSection") { + return ( + + ); + } else { + return showPageStudy(); + } +}; +export default Study; diff --git a/rslang/src/components/Study/StudySections/EmptySection.tsx b/rslang/src/components/Study/StudySections/EmptySection.tsx new file mode 100644 index 000000000..96b5e7f9f --- /dev/null +++ b/rslang/src/components/Study/StudySections/EmptySection.tsx @@ -0,0 +1,31 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./StudySections.scss"; +import React from "react"; +import { Container, Jumbotron, Button } from "react-bootstrap"; +import { BsX } from "react-icons/bs"; +interface InterfaceEmptySection { + onClosePage(str: string): void; +} + +const EmptySection: React.FC = (props) => { + const closePage = () => { + props.onClosePage(""); + }; + + return ( + + + Нет слов для изучения + + ); +}; + +export default EmptySection; diff --git a/rslang/src/components/Study/StudySections/HardWordsSection.tsx b/rslang/src/components/Study/StudySections/HardWordsSection.tsx new file mode 100644 index 000000000..b32ebd1fe --- /dev/null +++ b/rslang/src/components/Study/StudySections/HardWordsSection.tsx @@ -0,0 +1,305 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./StudySections.scss"; +import React, { useState, useEffect } from "react"; +import { + BsSkipEndFill, + BsArrowDown, + BsArrowUp, + BsArrowRight, + BsX, +} from "react-icons/bs"; +import { + Container, + Button, + Card, + ListGroup, + ListGroupItem, + Jumbotron, + Form, + ProgressBar, +} from "react-bootstrap"; +import EmptySection from "./EmptySection"; + +const url = `https://serene-falls-78086.herokuapp.com/`; + +interface InterfaceHardWordsSection { + words: any; + onClosePage(str: string): void; + onGetDeletedWords(arr: any): void; +} + +const HardWordsSection: React.FC = (props) => { + const [newWords, setNewWord] = useState(props.words); + const [wordCard, setWordCard] = useState(0); + const [show, setShow] = useState(false); + const [inputText, setInputText] = useState(""); + const [testButtonActivity, setTestButtonActivity] = useState(true); + const [testButtonText, setTestButtonText] = useState("Проверить"); + const [testButtonArrow, setTestButtonArrow] = useState(BsArrowUp); + const [hintButtonActivity, setHintButtonActivity] = useState(false); + const [textMeaning, setTextMeaning] = useState(""); + const [textExample, setTextExample] = useState(""); + + let [progressPercentage, setProgressPercentage] = useState(0); + let [cardNumber, setCardNumber] = useState(0); + + const getDeletedWords = () => { + props.onGetDeletedWords(newWords[cardNumber]); + setNewWord(newWords.filter((n: any) => n.id !== newWords[cardNumber].id)); + }; + + useEffect(() => { + setNewWord(props.words); + }, [props.words]); + + useEffect(() => { + setTestButtonText("Проверить"); + setTestButtonArrow(BsArrowUp); + }, [cardNumber]); + + useEffect(() => { + if (cardNumber <= newWords.length - 1) { + setTextMeaning( + newWords[cardNumber].textMeaning + .replace(`${newWords[cardNumber].word}`, "[.....]") + .replace( + `${ + newWords[cardNumber].word[0].toUpperCase() + + newWords[cardNumber].word.slice(1) + }`, + "[.....]" + ) + ); + setTextExample( + newWords[cardNumber].textExample + .replace(`${newWords[cardNumber].word}`, "[.....]") + .replace( + `${ + newWords[cardNumber].word[0].toUpperCase() + + newWords[cardNumber].word.slice(1) + }`, + "[.....]" + ) + ); + } + }, [cardNumber, wordCard]); + + useEffect(() => { + if (inputText.length !== 0) { + setTestButtonActivity(false); + } else { + setTestButtonActivity(true); + } + }, [inputText]); + + const enteredWord = (event: React.ChangeEvent) => { + setInputText(event.target.value); + }; + + const playAudio = () => { + const audioWord = new Audio(url + newWords[cardNumber].audio); + const audioMeaning = new Audio(url + newWords[cardNumber].audioMeaning); + const audioExample = new Audio(url + newWords[cardNumber].audioExample); + audioMeaning.pause(); + audioExample.pause(); + audioWord.addEventListener("ended", function () { + audioMeaning.play(); + }); + audioMeaning.addEventListener("ended", function () { + audioExample.play(); + }); + return audioWord.play(); + }; + const playAudioWord = () => { + const audioWord = new Audio(url + newWords[cardNumber].audio); + audioWord.play(); + }; + + const wordCheck = () => { + if (inputText.toLowerCase() === newWords[cardNumber].word.toLowerCase()) { + setTestButtonText("Следующее слово"); + setTestButtonArrow(BsArrowRight); + } else { + setTestButtonText("Проверить"); + setTestButtonArrow(BsArrowUp); + } + }; + + const keyPressHandler = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + wordCheck(); + } + }; + + const showNextCard = () => { + if (cardNumber < newWords.length && testButtonText === "Следующее слово") { + setCardNumber(1 + cardNumber); + setShow(false); + setInputText(""); + setHintButtonActivity(false); + setProgressPercentage(5 + progressPercentage); + } + }; + const getHint = () => { + setInputText(newWords[cardNumber].word); + setHintButtonActivity(true); + playAudioWord(); + }; + + const closePage = () => { + props.onClosePage(""); + }; + + useEffect(() => { + if (cardNumber > newWords.length - 1) { + setCardNumber(0); + setShow(false); + setProgressPercentage(0); + } else { + setWordCard( + + + + + {newWords[cardNumber].word} + + + + {newWords[cardNumber].transcription} + + + {newWords[cardNumber].wordTranslate} + + + + + {" "} + {textMeaning} + + {newWords[cardNumber].textMeaningTranslate} + + + + {textExample} + + {newWords[cardNumber].textExampleTranslate} + + + + + + + + + + + + + + + ); + } + }, [ + newWords, + show, + cardNumber, + testButtonActivity, + inputText, + testButtonText, + textMeaning, + ]); + + if (newWords.length === 0) { + return ; + } else { + return ( + + + +

+ Сегодня изучено: {cardNumber} из {newWords.length} слов +

+ +
+ {wordCard} +
+ ); + } +}; + +export default HardWordsSection; diff --git a/rslang/src/components/Study/StudySections/NewWordsSection.tsx b/rslang/src/components/Study/StudySections/NewWordsSection.tsx new file mode 100644 index 000000000..98c99d83d --- /dev/null +++ b/rslang/src/components/Study/StudySections/NewWordsSection.tsx @@ -0,0 +1,367 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./StudySections.scss"; +import React, { useState, useEffect } from "react"; +import { + BsSkipEndFill, + BsArrowDown, + BsArrowUp, + BsArrowRight, + BsX, +} from "react-icons/bs"; +import { + Container, + Button, + Card, + ListGroup, + ListGroupItem, + Jumbotron, + Form, + ProgressBar, +} from "react-bootstrap"; +import EmptySection from "./EmptySection"; +const url = `https://serene-falls-78086.herokuapp.com/`; + +interface InterfaceNewWordsSection { + words: any; + onClosePage(str: string): void; + onGetHardWords(arr: any): void; + onGetLearnedWords(arr: any): void; + onGetDeletedWords(arr: any): void; + onGetCorrectAnswer(arr: any): void; + onGetBestSeries(arr: any): void; + onGetLearnedWordToday(arr: any): void; +} + +const NewWordsSection: React.FC = (props) => { + const [newWords, setNewWord] = useState(props.words); + const [wordCard, setWordCard] = useState(0); + const [show, setShow] = useState(false); + const [inputText, setInputText] = useState(""); + const [testButtonActivity, setTestButtonActivity] = useState(true); + const [testButtonText, setTestButtonText] = useState("Проверить"); + const [testButtonArrow, setTestButtonArrow] = useState(BsArrowUp); + const [hintButtonActivity, setHintButtonActivity] = useState(false); + const [textMeaning, setTextMeaning] = useState(""); + const [textExample, setTextExample] = useState(""); + const [audio] = useState(new Audio("./sound/error.mp3")); + + const [correctAnswer, setCorrectAnswer] = useState(0); + let [wrongAnswer, setWrongAnswer] = useState(0); + let [bestAnswerSeries, setBestAnswerSeries] = useState(0); + + let [progressPercentage, setProgressPercentage] = useState(0); + let [cardNumber, setCardNumber] = useState(0); + + const correctAnswers = () => { + if ( + (inputText.toLowerCase() !== newWords[cardNumber].word.toLowerCase() || + inputText.toLowerCase() === newWords[cardNumber].word.toLowerCase()) && + testButtonText !== "Следующее слово" + ) { + setWrongAnswer(++wrongAnswer); + } + return setCorrectAnswer(Math.round(((cardNumber + 1) / wrongAnswer) * 100)); + }; + + const getBestAnswerSeries = () => { + if ( + inputText.toLowerCase() === newWords[cardNumber].word.toLowerCase() && + testButtonText !== "Следующее слово" + ) { + setBestAnswerSeries(++bestAnswerSeries); + if (!show) { + playAudioWord(); + } + } else if ( + inputText.toLowerCase() !== newWords[cardNumber].word.toLowerCase() && + testButtonText !== "Следующее слово" + ) { + setBestAnswerSeries(0); + audio.play(); + } + return bestAnswerSeries; + }; + + useEffect(() => { + props.onGetCorrectAnswer(correctAnswer); + }, [correctAnswer]); + + useEffect(() => { + props.onGetBestSeries(bestAnswerSeries); + }, [bestAnswerSeries]); + + useEffect(() => { + setNewWord(props.words); + }, [props.words]); + + const getHardWord = () => { + props.onGetHardWords(newWords[cardNumber]); + }; + const getDeletedWords = () => { + props.onGetDeletedWords(newWords[cardNumber]); + setNewWord(newWords.filter((n: any) => n.id !== newWords[cardNumber].id)); + }; + + useEffect(() => { + setTestButtonText("Проверить"); + setTestButtonArrow(BsArrowUp); + }, [cardNumber]); + + useEffect(() => { + if (cardNumber <= newWords.length - 1) { + setTextMeaning( + newWords[cardNumber].textMeaning + .replace(`${newWords[cardNumber].word}`, "[.....]") + .replace( + `${ + newWords[cardNumber].word[0].toUpperCase() + + newWords[cardNumber].word.slice(1) + }`, + "[.....]" + ) + ); + + setTextExample( + newWords[cardNumber].textExample + .replace(`${newWords[cardNumber].word}`, "[.....]") + .replace( + `${ + newWords[cardNumber].word[0].toUpperCase() + + newWords[cardNumber].word.slice(1) + }`, + "[.....]" + ) + ); + } + }, [cardNumber, wordCard]); + + useEffect(() => { + if (inputText.length !== 0) { + setTestButtonActivity(false); + } else { + setTestButtonActivity(true); + } + }, [inputText]); + + const enteredWord = (event: React.ChangeEvent) => { + setInputText(event.target.value); + }; + + const playAudio = () => { + const audioWord = new Audio(url + newWords[cardNumber].audio); + const audioMeaning = new Audio(url + newWords[cardNumber].audioMeaning); + const audioExample = new Audio(url + newWords[cardNumber].audioExample); + audioMeaning.pause(); + audioExample.pause(); + audioWord.addEventListener("ended", function () { + audioMeaning.play(); + }); + audioMeaning.addEventListener("ended", function () { + audioExample.play(); + }); + return audioWord.play(); + }; + const playAudioWord = () => { + const audioWord = new Audio(url + newWords[cardNumber].audio); + audioWord.play(); + }; + + const wordCheck = () => { + if (inputText.toLowerCase() === newWords[cardNumber].word.toLowerCase()) { + setTestButtonText("Следующее слово"); + setTestButtonArrow(BsArrowRight); + props.onGetLearnedWords(newWords[cardNumber]); + props.onGetLearnedWordToday(newWords[cardNumber]); + } else { + setTestButtonText("Проверить"); + setTestButtonArrow(BsArrowUp); + } + }; + + const keyPressHandler = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + wordCheck(); + } + }; + + const showNextCard = () => { + if (cardNumber < newWords.length && testButtonText === "Следующее слово") { + setCardNumber(1 + cardNumber); + setShow(false); + setInputText(""); + setHintButtonActivity(false); + setProgressPercentage(5 + progressPercentage); + } + }; + const getHint = () => { + setInputText(newWords[cardNumber].word); + setHintButtonActivity(true); + playAudioWord(); + }; + + const closePage = () => { + props.onClosePage(""); + }; + + useEffect(() => { + if (cardNumber > newWords.length - 1) { + setCardNumber(0); + setShow(false); + setProgressPercentage(0); + } else { + setWordCard( + + + + + {newWords[cardNumber].word} + + + + {newWords[cardNumber].transcription} + + + {newWords[cardNumber].wordTranslate} + + + + + {" "} + {textMeaning} + + {newWords[cardNumber].textMeaningTranslate} + + + + {textExample} + + {newWords[cardNumber].textExampleTranslate} + + + + + + + + + + + + + + + + ); + } + }, [ + newWords, + show, + cardNumber, + testButtonActivity, + inputText, + testButtonText, + textMeaning, + ]); + if (newWords.length === 0) { + return ; + } else { + return ( + + + +

+ Сегодня изучено: {cardNumber} из {newWords.length} слов +

+ +
+ {wordCard} +
+ ); + } +}; +export default NewWordsSection; diff --git a/rslang/src/components/Study/StudySections/RepeatWordsSection.tsx b/rslang/src/components/Study/StudySections/RepeatWordsSection.tsx new file mode 100644 index 000000000..9f5903386 --- /dev/null +++ b/rslang/src/components/Study/StudySections/RepeatWordsSection.tsx @@ -0,0 +1,316 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./StudySections.scss"; +import React, { useState, useEffect } from "react"; +import { + BsSkipEndFill, + BsArrowDown, + BsArrowUp, + BsArrowRight, + BsX, +} from "react-icons/bs"; +import { + Container, + Button, + Card, + ListGroup, + ListGroupItem, + Jumbotron, + Form, + ProgressBar, +} from "react-bootstrap"; +import EmptySection from "./EmptySection"; +const url = `https://serene-falls-78086.herokuapp.com/`; + +interface InterfaceRepeatWordsSection { + words: any; + onClosePage(str: string): void; + onGetHardWords(str: string): void; + onGetDeletedWords(arr: any): void; +} + +const RepeatWordsSection: React.FC = (props) => { + const [newWords, setNewWord] = useState(props.words); + const [wordCard, setWordCard] = useState(0); + const [show, setShow] = useState(false); + const [inputText, setInputText] = useState(""); + const [testButtonActivity, setTestButtonActivity] = useState(true); + const [testButtonText, setTestButtonText] = useState("Проверить"); + const [testButtonArrow, setTestButtonArrow] = useState(BsArrowUp); + const [hintButtonActivity, setHintButtonActivity] = useState(false); + const [textMeaning, setTextMeaning] = useState(""); + const [textExample, setTextExample] = useState(""); + + let [progressPercentage, setProgressPercentage] = useState(0); + let [cardNumber, setCardNumber] = useState(0); + + useEffect(() => { + setNewWord(props.words); + }, [props.words]); + + useEffect(() => { + setTestButtonText("Проверить"); + setTestButtonArrow(BsArrowUp); + }, [cardNumber]); + + useEffect(() => { + if (cardNumber <= newWords.length - 1) { + setTextMeaning( + newWords[cardNumber].textMeaning + .replace(`${newWords[cardNumber].word}`, "[.....]") + .replace( + `${ + newWords[cardNumber].word[0].toUpperCase() + + newWords[cardNumber].word.slice(1) + }`, + "[.....]" + ) + ); + + setTextExample( + newWords[cardNumber].textExample + .replace(`${newWords[cardNumber].word}`, "[.....]") + .replace( + `${ + newWords[cardNumber].word[0].toUpperCase() + + newWords[cardNumber].word.slice(1) + }`, + "[.....]" + ) + ); + } + }, [cardNumber, wordCard]); + + useEffect(() => { + if (inputText.length !== 0) { + setTestButtonActivity(false); + } else { + setTestButtonActivity(true); + } + }, [inputText]); + + const enteredWord = (event: React.ChangeEvent) => { + setInputText(event.target.value); + }; + + const playAudio = () => { + const audioWord = new Audio(url + newWords[cardNumber].audio); + const audioMeaning = new Audio(url + newWords[cardNumber].audioMeaning); + const audioExample = new Audio(url + newWords[cardNumber].audioExample); + audioMeaning.pause(); + audioExample.pause(); + audioWord.addEventListener("ended", function () { + audioMeaning.play(); + }); + audioMeaning.addEventListener("ended", function () { + audioExample.play(); + }); + return audioWord.play(); + }; + const playAudioWord = () => { + const audioWord = new Audio(url + newWords[cardNumber].audio); + audioWord.play(); + }; + + const wordCheck = () => { + if (inputText.toLowerCase() === newWords[cardNumber].word.toLowerCase()) { + setTestButtonText("Следующее слово"); + setTestButtonArrow(BsArrowRight); + } else { + setTestButtonText("Проверить"); + setTestButtonArrow(BsArrowUp); + } + }; + + const keyPressHandler = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + wordCheck(); + } + }; + + const showNextCard = () => { + if (cardNumber < newWords.length && testButtonText === "Следующее слово") { + setCardNumber(1 + cardNumber); + setShow(false); + setInputText(""); + setHintButtonActivity(false); + setProgressPercentage(5 + progressPercentage); + } + }; + const getHint = () => { + setInputText(newWords[cardNumber].word); + setHintButtonActivity(true); + playAudioWord(); + }; + const closePage = () => { + props.onClosePage(""); + }; + + const getHardWord = () => { + props.onGetHardWords(newWords[cardNumber]); + }; + + const getDeletedWords = () => { + props.onGetDeletedWords(newWords[cardNumber]); + setNewWord(newWords.filter((n: any) => n.id !== newWords[cardNumber].id)); + }; + useEffect(() => { + if (cardNumber > newWords.length - 1) { + setCardNumber(0); + setShow(false); + setProgressPercentage(0); + } else { + setWordCard( + + + + + {newWords[cardNumber].word} + + + + {newWords[cardNumber].transcription} + + + {newWords[cardNumber].wordTranslate} + + + + + {" "} + {textMeaning} + + {newWords[cardNumber].textMeaningTranslate} + + + + {textExample} + + {newWords[cardNumber].textExampleTranslate} + + + + + + + + + + + + + + + + ); + } + }, [ + newWords, + show, + cardNumber, + testButtonActivity, + inputText, + testButtonText, + textMeaning, + ]); + if (newWords.length === 0) { + return ; + } else { + return ( + + + +

+ Слов для повторения: {cardNumber} из {newWords.length} слов +

+ +
+ {wordCard} +
+ ); + } +}; + +export default RepeatWordsSection; diff --git a/rslang/src/components/Study/StudySections/StudySections.scss b/rslang/src/components/Study/StudySections/StudySections.scss new file mode 100644 index 000000000..c426bba9c --- /dev/null +++ b/rslang/src/components/Study/StudySections/StudySections.scss @@ -0,0 +1,22 @@ +.card-new-words { + margin: 0 auto; +} +.progress-line { + margin: 0 auto; +} +.study-page-head-text { + font-size: 1.2rem; + font-weight: 500; + line-height: 2rem; +} +.card { + transition: 1s; +} +.btn-close { + right: 0; +} +.empty-section-text { + font-size: 5rem; + font-weight: 800; +} + diff --git a/rslang/src/components/TutorialPage/TutorialPage.scss b/rslang/src/components/TutorialPage/TutorialPage.scss new file mode 100644 index 000000000..43247aa01 --- /dev/null +++ b/rslang/src/components/TutorialPage/TutorialPage.scss @@ -0,0 +1,39 @@ +.logo-tutorial-page { + font-weight: 900; + font-size: 2rem; + margin-top: 25px; + padding-right: 20px; + font-family: "Chango", cursive; + text-align: left; +} + +.user-name-and-avatar { + display: flex; + justify-content: center; + align-items: center; + + .avatar { + width: 40px; + height: 40px; + border-radius: 20px; + } + + .username { + text-align: center; + margin: 0 10px; + } +} +.user-name-and-avatar:hover { + font-weight: 600; + cursor: pointer; + color: rgb(1, 87, 248); +} +.user-name-and-avatar:active { + +} + +.login-button { + font-weight: 300; + padding: 7px 27px; + font-weight: 400; +} diff --git a/rslang/src/components/TutorialPage/TutorialPage.tsx b/rslang/src/components/TutorialPage/TutorialPage.tsx new file mode 100644 index 000000000..d08360418 --- /dev/null +++ b/rslang/src/components/TutorialPage/TutorialPage.tsx @@ -0,0 +1,34 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./TutorialPage.scss"; +import React from "react"; +import { Container } from "react-bootstrap"; +import { Link } from "react-router-dom"; +import { url } from "../../api/defData"; +import Menu from "../Menu/Menu"; +import Main from "../Main/Main"; +import SignOut from "../Signin/SignOut"; +interface InterfaceTutorialPage {} + +const TutorialPage: React.FC = (props) => { + const userpic: string | null = localStorage.getItem("userpic") || ""; + const username: string | null = localStorage.getItem("username") || ""; + const avatarUrl = `${url}${JSON.parse(userpic)}`; + + return ( + + + +

RS Lang

+ + + {userpic && avatar} + {username &&

{JSON.parse(username)}

} + + +
+ +
+ + ); +}; +export default TutorialPage; diff --git a/rslang/src/components/Vocabulary/Vocabulary.scss b/rslang/src/components/Vocabulary/Vocabulary.scss new file mode 100644 index 000000000..45c4e0b41 --- /dev/null +++ b/rslang/src/components/Vocabulary/Vocabulary.scss @@ -0,0 +1,15 @@ +.vocabulary-page-head-block { +} +.vocabulary-page-head { + font-size: 2rem; + font-weight: 900; +} +.vocabulary-page-head-text { + font-size: 1.2rem; + font-weight: 500; + line-height: 2rem; +} +.vocabulary-page-link { + font-weight: 500; + font-size: 1.1rem; +} diff --git a/rslang/src/components/Vocabulary/Vocabulary.tsx b/rslang/src/components/Vocabulary/Vocabulary.tsx new file mode 100644 index 000000000..67c9c97a5 --- /dev/null +++ b/rslang/src/components/Vocabulary/Vocabulary.tsx @@ -0,0 +1,105 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./Vocabulary.scss"; +import { useState, useEffect } from "react"; +import { Container, Nav, Button } from "react-bootstrap"; +import VocabularySections from "./VocabularySections/VocabularySections"; +import _ from "lodash"; + +interface InterfaceVocabulary { + hardWords: any; + learnedWords: any; + deletedWords: any; + sortingDeletedWords: any; + getHardWords(arr: any): void; + getLearnedWords(arr: any): void; + getDeletedWords(arr: any): void; +} + +const Vocabulary: React.FC = (props) => { + const [selectedSection, setSelectedSection] = useState( + "studied-sections" + ); + const [hardWords, setHardWords] = useState([]); + const [learnedWords, setLearnedWords] = useState([]); + const [deletedWords, setDeletedWords] = useState([]); + + useEffect(() => { + props.getHardWords(hardWords); + }, [hardWords]); + + useEffect(() => { + props.getLearnedWords(learnedWords); + }, [learnedWords]); + + useEffect(() => { + props.getDeletedWords(deletedWords); + }, [deletedWords]); + + const getHardWords = (arr: any) => { + setHardWords(_.uniqWith(hardWords.concat(arr), _.isEqual)); + }; + const getLearnedWords = (arr: any) => { + setLearnedWords(_.uniqWith(learnedWords.concat(arr), _.isEqual)); + }; + const getDeletedWords = (arr: any) => { + setDeletedWords(_.uniqWith(deletedWords.concat(arr), _.isEqual)); + }; + + const switchSection = (section: string): void => { + return setSelectedSection(section); + }; + + return ( + + +

Словарь.

+
+ + + + +
+ ); +}; +export default Vocabulary; diff --git a/rslang/src/components/Vocabulary/VocabularySections/DeletedVocabularySection.tsx b/rslang/src/components/Vocabulary/VocabularySections/DeletedVocabularySection.tsx new file mode 100644 index 000000000..065227ab5 --- /dev/null +++ b/rslang/src/components/Vocabulary/VocabularySections/DeletedVocabularySection.tsx @@ -0,0 +1,89 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./VocabularySections.scss"; +import React, { useState, useEffect } from "react"; +import { Container, Button } from "react-bootstrap"; + +interface InterfaceDeletedVocabularySection { + deletedWords: any; + onGetHardWords(arr: any): void; + onGetLearnedWords(arr: any): void; +} + +const DeletedVocabularySection: React.FC = ( + props +) => { + const [allWords, setAllWords] = useState(props.deletedWords); + const [wordList, setWordList] = useState([]); + const [selectedWords, setSelectedWords] = useState([]); + const wordsToMove: any = []; + + const wordDistribution = () => { + setAllWords( + allWords.filter( + (e: any) => selectedWords.findIndex((i: any) => i === e.word) === -1 + ) + ); + setSelectedWords([]); + }; + + const studiedtWords = () => { + props.onGetLearnedWords( + wordsToMove.concat( + allWords.filter((element: any) => selectedWords.includes(element.word)) + ) + ); + }; + + const handleChange = (e: any) => { + if (e.target.checked) { + setSelectedWords([...selectedWords, e.target.value]); + } else { + setSelectedWords( + selectedWords.filter((value: string) => value !== e.target.value) + ); + } + }; + + useEffect(() => { + setWordList( + allWords.map((item: any) => { + return ( +
  • + + {item.word} +
  • + ); + }) + ); + }, [allWords]); + + return ( + + + + + +

    + Выбрано {selectedWords.length} слов +

    +
      + {wordList} +
    +
    +
    + ); +}; +export default DeletedVocabularySection; diff --git a/rslang/src/components/Vocabulary/VocabularySections/HardVocabularySection.tsx b/rslang/src/components/Vocabulary/VocabularySections/HardVocabularySection.tsx new file mode 100644 index 000000000..cf8b5b513 --- /dev/null +++ b/rslang/src/components/Vocabulary/VocabularySections/HardVocabularySection.tsx @@ -0,0 +1,104 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./VocabularySections.scss"; +import React, { useState, useEffect } from "react"; +import { Container, Button } from "react-bootstrap"; +interface InterfaceHardVocabularySection { + hardWords: any; + onGetLearnedWords(arr: any): void; + onGetDeletedWords(arr: any): void; +} + +const HardVocabularySection: React.FC = ( + props +) => { + const [allWords, setAllWords] = useState(props.hardWords); + const [wordList, setWordList] = useState([]); + const [selectedWords, setSelectedWords] = useState([]); + const wordsToMove: any = []; + + const wordDistribution = () => { + setAllWords( + allWords.filter( + (e: any) => selectedWords.findIndex((i: any) => i === e.word) === -1 + ) + ); + setSelectedWords([]); + }; + + const studiedtWords = () => { + props.onGetLearnedWords( + wordsToMove.concat( + allWords.filter((element: any) => selectedWords.includes(element.word)) + ) + ); + }; + const deletedWords = () => { + props.onGetDeletedWords( + wordsToMove.concat( + allWords.filter((element: any) => selectedWords.includes(element.word)) + ) + ); + }; + const handleChange = (e: any) => { + if (e.target.checked) { + setSelectedWords([...selectedWords, e.target.value]); + } else { + setSelectedWords( + selectedWords.filter((value: string) => value !== e.target.value) + ); + } + }; + + useEffect(() => { + setWordList( + allWords.map((item: any) => { + return ( +
  • + + {item.word} +
  • + ); + }) + ); + }, [allWords]); + + return ( + + + + + + +

    + Выбрано {selectedWords.length} слов +

    +
      + {wordList} +
    +
    +
    + ); +}; +export default HardVocabularySection; diff --git a/rslang/src/components/Vocabulary/VocabularySections/StudiedVocabularySection.tsx b/rslang/src/components/Vocabulary/VocabularySections/StudiedVocabularySection.tsx new file mode 100644 index 000000000..47a7f4435 --- /dev/null +++ b/rslang/src/components/Vocabulary/VocabularySections/StudiedVocabularySection.tsx @@ -0,0 +1,116 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./VocabularySections.scss"; +import React, { useState, useEffect } from "react"; +import { Container, Button } from "react-bootstrap"; + +interface InterfaceStudiedVocabularySection { + learnedWords: any; + sortingDeletedWords: any; + onGetHardWords(arr: any): void; + onGetLearnedWords(arr: any): void; + onGetDeletedWords(arr: any): void; +} + +const StudiedVocabularySection: React.FC = ( + props +) => { + + const [allWords, setAllWords] = useState(props.sortingDeletedWords); + const [wordList, setWordList] = useState([]); + const [selectedWords, setSelectedWords] = useState([]); + const wordsToMove: any = []; + + useEffect(() => { + setAllWords(props.sortingDeletedWords); + }, [props.sortingDeletedWords]); + + const hardWords = () => { + props.onGetHardWords( + wordsToMove.concat( + allWords.filter((element: any) => selectedWords.includes(element.word)) + ) + ); + }; + const deletedWords = () => { + props.onGetDeletedWords( + wordsToMove.concat( + allWords.filter((element: any) => selectedWords.includes(element.word)) + ) + ); + }; + + const wordDistribution = () => { + setAllWords( + allWords.filter( + (e: any) => selectedWords.findIndex((i: any) => i === e.word) === -1 + ) + ); + + setSelectedWords([]); + }; + + const handleChange = (e: any) => { + if (e.target.checked) { + setSelectedWords([...selectedWords, e.target.value]); + } else { + setSelectedWords( + selectedWords.filter((value: string) => value !== e.target.value) + ); + } + }; + + useEffect(() => { + setWordList( + allWords.map((item: any) => { + return ( +
  • + + {item.word} +
  • + ); + }) + ); + }, [allWords]); + + return ( + + + + + + + +

    + Выбрано {selectedWords.length} слов +

    +
      + {wordList} +
    +
    +
    + ); +}; + +export default StudiedVocabularySection; diff --git a/rslang/src/components/Vocabulary/VocabularySections/VocabularySections.scss b/rslang/src/components/Vocabulary/VocabularySections/VocabularySections.scss new file mode 100644 index 000000000..0bd032208 --- /dev/null +++ b/rslang/src/components/Vocabulary/VocabularySections/VocabularySections.scss @@ -0,0 +1,13 @@ +.selected-words-head { + font-size: 1.2rem; + font-weight: 500; +} +.list-word { + display: flex; + align-items: center; + padding-left: 3.5rem; + font-size: 1.2rem; +} +.list-word-checkbox { + margin-left: -2.25rem; +} diff --git a/rslang/src/components/Vocabulary/VocabularySections/VocabularySections.tsx b/rslang/src/components/Vocabulary/VocabularySections/VocabularySections.tsx new file mode 100644 index 000000000..4675f9e23 --- /dev/null +++ b/rslang/src/components/Vocabulary/VocabularySections/VocabularySections.tsx @@ -0,0 +1,79 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "./VocabularySections.scss"; +import React, { useState, useEffect } from "react"; +import _ from "lodash"; +import StudiedVocabularySection from "./StudiedVocabularySection"; +import HardVocabularySection from "./HardVocabularySection"; +import DeletedVocabularySection from "./DeletedVocabularySection"; +interface InterfaceVocabularySections { + selectedSection: string; + hardWords: any; + learnedWords: any; + deletedWords: any; + sortingDeletedWords: any; + getHardWords(arr: any): void; + getLearnedWords(arr: any): void; + getDeletedWords(arr: any): void; +} + +const VocabularySections: React.FC = (props) => { + const [hardWords, setHardWords] = useState([]); + const [learnedWords, setLearnedWords] = useState([]); + const [deletedWords, setDeletedWords] = useState([]); + + useEffect(() => { + props.getHardWords(hardWords); + }, [hardWords]); + + useEffect(() => { + props.getLearnedWords(learnedWords); + }, [learnedWords]); + + useEffect(() => { + props.getDeletedWords(deletedWords); + }, [deletedWords]); + + const getHardWords = (arr: any) => { + setHardWords(_.uniqWith(hardWords.concat(arr), _.isEqual)); + }; + + const getLearnedWords = (arr: any) => { + setLearnedWords(_.uniqWith(learnedWords.concat(arr), _.isEqual)); + }; + + const getDeletedWords = (arr: any) => { + setDeletedWords(_.uniqWith(deletedWords.concat(arr), _.isEqual)); + }; + + const showSelectedSection = () => { + if (props.selectedSection === "hard-sections") { + return ( + + ); + } else if (props.selectedSection === "deleted-sections") { + return ( + + ); + } else { + return ( + + ); + } + }; + return showSelectedSection(); +}; +export default VocabularySections; diff --git a/rslang/src/hooks/useLocalStorage.tsx b/rslang/src/hooks/useLocalStorage.tsx new file mode 100644 index 000000000..c1f199de6 --- /dev/null +++ b/rslang/src/hooks/useLocalStorage.tsx @@ -0,0 +1,28 @@ +import { useState } from "react"; + +const useLocalStorage = (key: any, initialValue: any) => { + const [storedValue, setStoredValue] = useState(() => { + // window.localStorage.clear(); + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.log(error); + return initialValue; + } + }); + const setValue = (value: any) => { + try { + const valueToStore = + value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } catch (error) { + console.log(error); + } + }; + + return [storedValue, setValue]; +}; + +export default useLocalStorage; diff --git a/rslang/src/index.css b/rslang/src/index.css new file mode 100644 index 000000000..4a1df4db7 --- /dev/null +++ b/rslang/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} diff --git a/rslang/src/index.tsx b/rslang/src/index.tsx new file mode 100644 index 000000000..eec0ac9c7 --- /dev/null +++ b/rslang/src/index.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import "./index.css"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; + +ReactDOM.render( + + + , + document.getElementById("root") +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/rslang/src/react-app-env.d.ts b/rslang/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/rslang/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/rslang/src/reportWebVitals.ts b/rslang/src/reportWebVitals.ts new file mode 100644 index 000000000..49a2a16e0 --- /dev/null +++ b/rslang/src/reportWebVitals.ts @@ -0,0 +1,15 @@ +import { ReportHandler } from 'web-vitals'; + +const reportWebVitals = (onPerfEntry?: ReportHandler) => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/rslang/src/setupTests.ts b/rslang/src/setupTests.ts new file mode 100644 index 000000000..8f2609b7b --- /dev/null +++ b/rslang/src/setupTests.ts @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/rslang/src/utils/AudioWord.tsx b/rslang/src/utils/AudioWord.tsx new file mode 100644 index 000000000..a553df1bc --- /dev/null +++ b/rslang/src/utils/AudioWord.tsx @@ -0,0 +1,11 @@ +import { url } from "./../api/defData"; + +export const playAudioWord = (audioPath: string) => { + const audioWord = new Audio(url + audioPath); + audioWord.play(); +}; + +export const playAudio = (audio: any) => { + const audioWord = new Audio(audio); + audioWord.play(); +}; diff --git a/rslang/tsconfig.json b/rslang/tsconfig.json new file mode 100644 index 000000000..a273b0cfc --- /dev/null +++ b/rslang/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} diff --git a/task.md b/task.md new file mode 100644 index 000000000..556646f16 --- /dev/null +++ b/task.md @@ -0,0 +1,219 @@ +https://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-rslang.md + +RS Lang – приложение для изучения иностранных слов, включающее электронный учебник с базой слов для изучения, мини-игры для их повторения, страницу статистики для отслеживания индивидуального прогресса. + +электронная версия учебника "4000 Essential English Words" - из этого учебника необходимо воспроизвести Word List +приложение Lingualeo - из этого приложения необходимо воспроизвести мини-игры "Саванна", "Аудиовызов", "Спринт". +Для доступа к играм "Аудиовызов" и "Спринт" понадобится Lingualeo Premium. Бесплатный доступ к Lingualeo Premium на один день откроется после 5 дней тренировки. Также для знакомства с геймплеем можно использовать видео: Саванна и Аудиовызов, Спринт + +### Лучшие работы студентов предыдущего набора +(сейчас требования к заданию изменились) + +https://rslang-team16-arcanar7.web.app/ + +https://rslang-team41-jekman87.netlify.app/ + +https://rslang-team5-alekchaik.netlify.app/ + +https://rslang-team11-kagafon.netlify.app/ + +https://rslang-team69-dimonwhite.netlify.app/ + +https://rslang-team26-evgender.netlify.app/ + +https://rslang-team64-viktorsipach.netlify.app/ + +### Исходные данные +Коллекция "4000 essential english words". Коллекция содержит 3600 часто употребляемых английских слов, изучение которых вам необходимо организовать. Слова в коллекции отсортированы от более простых и известных к более сложным. Первые 400 наиболее часто употребляемых слов в коллекцию не вошли. Считается, что это базовый запас взрослого человека, оставшийся от предыдущих попыток изучения языка. Вся коллекция разбита на шесть групп, в каждой группе 30 страниц, на каждой странице 20 слов для изучения. + +## Структура приложения + +- главная страница приложения + +- электронный учебник со словарём + +- мини-игры "Саванна", "Аудиовызов", "Спринт", "Своя игра" + +- страница статистики + +### Описание функциональных блоков + +#### 1 Главная страница приложения +выполняет функцию промо-страницы, её оформление определяет первое впечатление о приложении +главная страница приложения содержит: +- меню с навигацией по учебнику, ссылками на мини-игры и статистику. Меню или иконка меню отображается на всех страницах приложения +- описание возможностей и преимуществ приложения +- небольшое (5-7 минут) видео с демонстрацией работы приложения +- раздел "О команде" с фото и ссылками на гитхабы всех участников команды, описанием вклада в разработку приложения каждого из них. При желании данный раздел можно вынести в отдельную страницу +- footer со ссылками на гитхабы авторов приложения, год создания приложения, логотип курса со ссылкой на курс. footer отображается на всех страницах приложения за исключением мини-игр. + + #### 2 Электронный учебник + - электронный учебник состоит из шести разделов, которым соответствуют шесть групп слов коллекции исходных данных. В каждом разделе 30 страниц. На каждой странице выводится: + - меню или иконка меню + - иконка настроек + - список из 20 слов + - ссылки на мини-игры "Саванна", "Аудиовызов", "Спринт", "Своя игра" для повторения изученных слов + - навигация по страницам со стрелками для перехода к следующей и предыдущей страницам и номером текущей страницы + - также необходимо продумать навигацию по шести разделам учебника и предусмотреть небольшие различия в оформлении каждого раздела. Например, можно использовать для каждого раздела индикатор определённого цвета + - при перезагрузке страницы открывается последняя открытая страница приложения + + ##### Настройки + - в настройках учебника у пользователя есть возможность указать: + - нужно ли отображать в списке слов перевод изучаемого слова и перевод предложений с ним + - нужно ли отображать возле каждого слова кнопки, при клике по которым данное слово добавляется в раздел словаря "Сложные слова" или "Удалённые слова" + + ##### Список слов + - для каждого слова отображается: + - само слово, его транскрипция, его перевод + - предложение с объяснением значения изучаемого слова, его перевод + - предложение с примером использования изучаемого слова, его перевод + - картинка-ассоциация к изучаемому слову + - иконка аудио при клике по которой последовательно звучит произношение изучаемого слова, произношение предложения с объяснением его значения, произношение предложения с примером его использования + - кнопки, при клике по которым изучаемое слово добавляется в разделы словаря "Сложные слова" или "Удалённые слова" + - результат изучения/повторения слова в мини-играх + - если слово добавлено в раздел словаря "Сложные слова", оно остаётся на странице учебника, и его стиль изменяется или возле него выводится индикатор, указывающий, что оно относится к сложным словам + - если слово добавлено в раздел словаря "Удалённые слова", оно удаляется со страницы учебника. Если пользователь удалит со страницы все слова, страница удаляется + + ##### Словарь + - словарь является частью учебника. В словаре есть разделы "Изучаемые слова", "Сложные слова", "Удалённые слова" + - в раздел "Изучаемые слова" попадают слова, которые были задействованы в мини-играх, если мини-игры открывались кликом по ссылке на странице учебника или на странице раздела словаря "Сложные слова". Также в раздел "Изучаемые слова" попадают слова, которые пользователь отметил как сложные. Возле сложных слов есть индикатор или они выделены стилем, так же, как и на странице учебника + - возле каждого слова в разделе "Изучаемые слова" указывается результат изучения - сколько раз слово было правильно угадано в мини-играх, сколько раз пользователь ошибался + - для каждого раздела и каждой страницы учебника указывается количество изучаемых слов и общий результат их изучения + - в разделы словаря "Сложные слова" и "Удалённые слова" слова попадают, если пользователь кликнул по соответствующим кнопкам возле слов на страницах учебника + - страницы разделов словаря "Сложные слова" и "Удалённые слова" выглядят точно так же, как страницы учебника: формируются страницы, на каждой из которых список из 20 слов, создаётся новая страница, на страницах есть ссылки на мини-игры для повторения слов. Слова из разных разделов учебника попадают на разные страницы, на странице есть индикатор, указывающий, к какому разделу учебника она относитеся. Если слов больше 20, создаётся новая страница. Единственное отличие в списке слов вместо кнопок, при клике по которым изучаемое слово добавляется в разделы словаря "Сложные слова" или "Удалённые слова", в словаре возле слова отображается кнопка "Восстановить", которая удаляет слово из словаря и восстанавливает его на странице электронного учебника + + #### Мини-игры "Саванна", "Аудиовызов", "Спринт", "Своя игра" + - мини-игры предназначены для изучения и повторения слов электронного учебника + - мини-игры "Саванна", "Аудиовызов" и "Спринт" повторяют одноимённые мини-игры приложения Lingualeo + - мини-игру с условным названием "Своя игра" вы придумываете сами + - слова, которые используются в мини-играх, отличаются в зависимости от того, откуда вы открываете игру: по ссылке в меню или по ссылке на странице учебника + - если мини-игра открывается по ссылке в меню, в ней есть возможность выбрать один из шести уровней сложности. Уровень сложности мини-игры определяет раздел учебника, слова из которого в ней будут использоваться, + - если мини-игра открывается по ссылке на странице учебника, она используется для повторения слов, размещённых на этой странице. В этом случае в игре нет возможности выбрать уровень сложности и используются те слова, которые размещены на данной странице учебника. Если 20 слов для мини-игры не хватает, в ней задействуются слова из предыдущих страниц учебника. Если предыдущих страниц нет или недостаточно, игру можно заканчивать досрочно, когда исчерпаются все доступные слова + - слова, которые использовались в мини-играх, открытых по ссылке на странице учебника или на странице раздела словаря "Сложные слова", попадают в раздел словаря "Изучаемые слова" + + #### Страница статистики + - на странице статистики отображается краткосрочная статистика по результатам каждого дня и долгосрочная статистика за весь период изучения + - в краткосрочной статистике указывается количество изученных слов, процент правильных ответов и самая длинная серия правильных ответов по каждой мини-игре отдельно, а также общее количество изученных слов и процент правильных ответов за день + - в долгосрочной статистике представлены два графика. На одном из них отображается количество изученных слов за каждый день изучения, на другом - увеличение общего количества изученных слов за весь период изучения по дням. + + #### Бекенд + + Что уже есть: + + создан репозиторий с бекендом на его основе создан ReactLearnWords API, позволяющий получить исходные данные + + создана ReactLearnWords wiki с инструкциями по созданию базы данных MongoDB, деплою бекенда на heroku, примерами получения исходных данных + + Что нужно сделать: + создать свою копию бекенда. Для этого: форкните репозиторий с бекендом для создания базы данных MongoDB и деплоя бекенда на heroku следуйте туториалам ReactLearnWords wiki + + - вам необходимо добавить в бекенд возможность при регистрации нового пользователя указать его имя и загрузить фото + +### Технические требования +- работа приложения проверяется в браузере Google Chrome последней версии +- необходимо использовать React +- можно использовать bootstrap, material design, css-фреймворки, html и css препроцессоры +- можно использовать js-библиотеки +- разрешается использовать jQuery только в качесте подключаемой зависимости для UI библиотек. Использование jQuery в основном коде приложения не допускается +- рекомендуется использовать TypeScript +- рекомендуется создать и использовать бекенд. Данная рекомендация связана с очень высоким спросом на фронтенд-разработчиков, знакомых хотя бы с основами node.js. +запрещено копировать код других студентов, демо, примеров, которые приводятся в задании. Этот запрет касается html, css, js кода. Можно использовать небольшие фрагменты кода со Stack Overflow, других самостоятельно найденных источников в интернете, за исключением github-репозиториев студентов курса. Возле использованного чужого фрагмента кода в комментарии указывается ссылка на источник. + +### Как сабмитить задание +Участникам команд необходимо записаться в таблицу, ссылка на которую будет размещена в анонсах + +Ссылку на pull request в rs app сабмитит только тимлид + +Убедитесь, что pull request доступен для проверки. Для этого откройте ссылку, которую сабмитите в rs app, в режиме инкогнито браузера + +Если задание не засабмитить до дедлайна, оно не попадёт на распределение при кросс-чеке и за него не будут выставлены баллы + +### Требования к оформлению приложения +#### особое внимание обратите на качество оформления приложения. +Как прототип можно использовать подходящие шаблоны, размещённые на behance, dribbble, pinterest + +Качественное приложение характеризуется проработанностью деталей, вниманием к типографике (не больше трёх шрифтов на странице, размер шрифта не меньше 14 рх, оптимальная контрастность шрифта и фона), тщательно подобранным контентом +вёрстка адаптивная. Минимальная ширина страницы, при которой проверяется корректность отображения приложения - 500рх + +Интерактивность элементов, с которыми пользователи могут взаимодействовать, изменение внешнего вида самого элемента и состояния курсора при наведении, использование разных стилей для активного и неактивного состояния элемента, плавные анимации +единство стилей всех страниц приложения - одинаковые шрифты, стили кнопок, отступы, одинаковые элементы на всех страницах приложения имеют одинаковый внешний вид и расположение. Цвет элементов и фоновые изображения могут отличаться. В этом случае цвета используются из одной палитры, а фоновые изображения из одной коллекции. + +#### Требования к мини-играм +- все игры выполнены в одном стиле, при этом в оформлении каждой игры есть индивидуальные отличия (цветовая схема, фоновый рисунок, эффекты анимации и т. д.) +мини-игру можно развернуть во весь экран +- по окончанию каждой игры выводятся результаты мини-игры +- одинаковые элементы игр, такие как результаты мини-игры, блок выбора уровня сложности, стартовый экран, если он есть, и т.д. идентичны по внешнему виду, расположению на странице, функционалу +- управлять игрой можно как мышкой, так и клавишами на клавиатуре, как это реализовано в оригинальных играх +- если мини-игра запускается из меню, в ней можно выбрать один из шести уровней сложности, которые отличаются тем, слова какой из шести частей коллекции исходных данных в ней задействованы +- если мини-игра запускается со страницы учебника, в ней используются слова из той страницы учебника, на которой размещена ссылка на игру. Если размещённых на странице слов для игры недостаточно, задействуются слова с предыдущих страниц + +### Критерии оценивания + +Максимальный балл за задание 600 + +500 баллов за приложение + +100 баллов за презентацию + +Для удобства проверки необходимо записать и разместить на YouTube небольшое (5-7 мин) видео для проверяющих с объяснением как реализован каждый пункт из перечисленных в критериях оценки. Особое внимание обратите на те пункты критериев оценки, которые проверяющий проверить не сможет, например, на то как вы реализовали базу данных, как задеплоили бекенд, как выглядит долгосрочная статистика и т.д. Ссылку на видео можно добавить в описание pull request или в footer приложения добавить иконку YouTube со ссылкой на видео. + +При оценивании приложения проверяются все требования, описанные в пунктах Описание функциональных блоков, Требования к оформлению приложения, Требования к мини-играм. Если какие-то из перечисленных требований не выполняются, снимаем часть баллов. В комментарии к оценке необходимо указать какие пункты не выполнены или выполнены частично. + +#### Вёрстка, дизайн, UI +40 +- вёрстка, дизайн, UI главной страницы приложения +10 +- вёрстка, дизайн, UI электронного учебника +10 +- вёрстка, дизайн, UI страницы статистики +10 +- оригинальный интересный качественный дизайн приложения +10 + +#### Главная страница приложения +40 +- меню +10 +- описание возможностей и преимуществ приложения +10 +- видео с демонстрацией работы приложения +10 +- раздел "О команде" +10 + +#### Электронный учебник +50 +- страницы и разделы учебника +10 +- настройки +10 +- список слов +20 +- навигация по страницам и разделам учебника +10 + +#### Словарь +40 +- раздел "Изучаемые слова" +20 +- раздел "Сложные слова" +10 +- раздел "Удалённые слова" +10 + +#### Мини-игры +200 (максимум +50 баллов за каждую игру) + +Мини-игра может оцениваться в 30, 40 или 50 баллов. + +При оценке предложенной командой игры, её сложность, интересность, полезность, качество реализации сравнивается с другими мини-играми и оценивается по сравнению с ними. + +- игра в основном соответствует прототипу, является его упрощённой версией +30 + +- игра полностью повторяет прототип и детали его работы. Выполняются все перечисленные в задании требования к мини-играм +40 + +- игра является улучшенной версией прототипа как с точки зрения внешнего вида и оформления, так и удобства работы. Присутствует дополнительный функционал, улучшающий качество приложения +50 + +#### Страница статистики +40 + +- краткосрочная статистика +20 + +- долгосрочная статистика +20 + +#### Бекенд +60 + +- собственная копия бекенда размещена на heroku или другом бесплатном хостинге +20 + +- приложение использует данные из собственного API +10 + +- при регистрации нового пользователя можно указать его имя. При перезагрузке клиента данные о пользователе сохраняются +10 + +- при регистрации нового пользователя можно загрузить фото +10 + +- реализована авторизация и разавторизация пользователя. Основная часть приложения доступна без авторизации. Авторизация необходима только для хранения долгосрочной статистики и формирования словаря +10 + +#### Дополнительный функционал +30** + +- реализован не указанный в задании дополнительный функционал. Оценивается оригинальная идея, вклад в улучшение качества приложения, полезность, сложность и качество выполнения +20 + +- написано не меньше 10 юнит-тестов, использующих различные методы jest +10