diff --git a/now.json b/now.json index 04857b4..48ebe7e 100644 --- a/now.json +++ b/now.json @@ -23,6 +23,7 @@ { "src": "/manifest.json", "dest": "/manifest.json" }, { "src": "/asset-manifest.json", "dest": "/asset-manifest.json" }, { "src": "/precache-manifest.(.*)", "dest": "/precache-manifest.$1" }, - { "src": "/(.*)", "headers": { "cache-control": "s-maxage=0" }, "dest": "/index.html" } + { "src": "/", "headers": { "cache-control": "s-maxage=0" }, "dest": "/index.html" }, + { "src": "/(.*)", "status": 301, "headers": { "Location": "/" }} ] } diff --git a/package-lock.json b/package-lock.json index 24c1378..34501a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -919,7 +919,6 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", "integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==", - "dev": true, "requires": { "regenerator-runtime": "^0.12.0" }, @@ -927,8 +926,7 @@ "regenerator-runtime": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", - "dev": true + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" } } }, @@ -2000,8 +1998,7 @@ "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, "asn1": { "version": "0.2.4", @@ -3254,6 +3251,11 @@ } } }, + "change-emitter": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", + "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=" + }, "character-entities": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.2.tgz", @@ -3997,6 +3999,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-css": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", @@ -4457,6 +4464,15 @@ "sha.js": "^2.4.8" } }, + "create-react-context": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.3.tgz", + "integrity": "sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==", + "requires": { + "fbjs": "^0.8.0", + "gud": "^1.0.0" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -5402,6 +5418,14 @@ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -6478,6 +6502,35 @@ "bser": "^2.0.0" } }, + "fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + } + } + }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", @@ -7918,6 +7971,11 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "dev": true }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "gzip-size": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.0.0.tgz", @@ -8135,6 +8193,19 @@ "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", "dev": true }, + "history": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/history/-/history-4.9.0.tgz", + "integrity": "sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^2.2.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^0.4.0" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -8152,6 +8223,11 @@ "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", "dev": true }, + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -8677,7 +8753,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -9229,8 +9304,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-supported-regexp-flag": { "version": "1.0.1", @@ -9319,6 +9393,15 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -11095,6 +11178,15 @@ "lower-case": "^1.1.1" } }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, "node-forge": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", @@ -13447,6 +13539,65 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-router": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.0.tgz", + "integrity": "sha512-6EQDakGdLG/it2x9EaCt9ZpEEPxnd0OCLBHQ1AcITAAx7nCnyvnzf76jKWG1s2/oJ7SSviUgfWHofdYljFexsA==", + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "^0.2.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz", + "integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==", + "requires": { + "react-is": "^16.7.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "react-router-dom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.0.0.tgz", + "integrity": "sha512-wSpja5g9kh5dIteZT3tUoggjnsa+TPFHSMrpHXMpFsaHhQkm/JNVGh2jiF9Dkh4+duj4MKCkwO6H08u6inZYgQ==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + }, "react-scripts": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-2.1.8.tgz", @@ -13869,6 +14020,19 @@ "util.promisify": "^1.0.0" } }, + "recompose": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz", + "integrity": "sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==", + "requires": { + "@babel/runtime": "^7.0.0", + "change-emitter": "^0.1.2", + "fbjs": "^0.8.1", + "hoist-non-react-statics": "^2.3.1", + "react-lifecycles-compat": "^3.0.2", + "symbol-observable": "^1.0.4" + } + }, "recursive-readdir": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", @@ -14272,6 +14436,11 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "resolve-pathname": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -14376,8 +14545,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sane": { "version": "2.5.2", @@ -14928,8 +15096,7 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "setprototypeof": { "version": "1.1.0", @@ -16245,6 +16412,11 @@ "util.promisify": "~1.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", @@ -16505,6 +16677,16 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "tiny-invariant": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.4.tgz", + "integrity": "sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g==" + }, + "tiny-warning": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.2.tgz", + "integrity": "sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q==" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -16724,6 +16906,11 @@ "integrity": "sha512-jjOcCZvpkl2+z7JFn0yBOoLQyLoIkNZAs/fYJkUG6VKy6zLPHJGfQJYFHzibB6GJaF/8QrcECtlQ5cpvRHSMEA==", "dev": true }, + "ua-parser-js": { + "version": "0.7.19", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz", + "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==" + }, "uglify-js": { "version": "3.4.10", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", @@ -17084,6 +17271,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", + "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -17865,8 +18057,7 @@ "whatwg-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", - "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==", - "dev": true + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" }, "whatwg-mimetype": { "version": "2.3.0", diff --git a/package.json b/package.json index 04a3e41..8eacc04 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,11 @@ "version": "0.1.0", "private": true, "dependencies": { + "classnames": "2.2.6", "react": "16.8.5", - "react-dom": "16.8.5" + "react-dom": "16.8.5", + "react-router-dom": "5.0.0", + "recompose": "0.30.0" }, "devDependencies": { "@hellroot/eslint-config": "1.7.1", diff --git a/public/favicon.ico b/public/favicon.ico index a11777c..8fd60d6 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html index 9a8ef8f..465746e 100644 --- a/public/index.html +++ b/public/index.html @@ -7,8 +7,7 @@ name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - - React App + Входящие — Яндекс.Почта diff --git a/src/app/app.css b/src/app/app.css deleted file mode 100644 index 1c4d511..0000000 --- a/src/app/app.css +++ /dev/null @@ -1,27 +0,0 @@ -.app { - text-align: center; -} - -.app-header { - display: flex; - min-height: 100vh; - flex-direction: column; - align-items: center; - justify-content: center; - background-color: #282c34; - color: #fff; - font-size: calc(10px + 2vmin); -} - -.app-link { - color: #61dafb; -} - -@keyframes app-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/app/app.jsx b/src/app/app.jsx deleted file mode 100644 index f759eed..0000000 --- a/src/app/app.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { Component } from 'react'; - -import './app.css'; - -export class App extends Component { - render() { - return ( -
-
-

- Edit src/app/app.jsx and save to reload. -

- - Learn React - -
-
- ); - } -} diff --git a/src/app/app.test.jsx b/src/app/app.test.jsx deleted file mode 100644 index 81be6fa..0000000 --- a/src/app/app.test.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import { App } from './app'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/src/components/actions-box/actions-box.jsx b/src/components/actions-box/actions-box.jsx new file mode 100644 index 0000000..4e317a4 --- /dev/null +++ b/src/components/actions-box/actions-box.jsx @@ -0,0 +1,50 @@ +import React, { useContext } from 'react'; +import classNames from 'classnames'; +import { mem } from '../../react-utils'; + +import { Checkbox } from '../checkbox'; +import { List } from '../list'; +import { Action } from './actions'; +import { DeleteLetterAction } from './actions/delete-letters-action'; +import { ReadLettersAction } from './actions/read-letters-action'; +import { LettersContext } from '../letters-context'; + +import styles from './actions-box.module.css'; + +export const ActionsBox = ({ className, disabled }) => { + const { getLetters, changeLetters } = useContext(LettersContext); + const letters = mem(() => getLetters()).deps(getLetters); + + const handleChange = event => { + const { checked } = event.target; + changeLetters(() => ({ checked })); + }; + + const checkboxChecked = mem( + () => !disabled && letters.length > 0 && letters.every(letter => letter.checked) + ).deps(letters); + + const checkboxEnabled = mem(() => !disabled && letters.length > 0).deps(letters); + + const actionsEnabled = mem(() => !disabled && letters.some(letter => letter.checked)).deps( + letters + ); + + return ( +
+ + + Переслать + + Это спам! + + +
+ ); +}; diff --git a/src/components/actions-box/actions-box.module.css b/src/components/actions-box/actions-box.module.css new file mode 100644 index 0000000..33afd97 --- /dev/null +++ b/src/components/actions-box/actions-box.module.css @@ -0,0 +1,35 @@ +.actions_box { + display: flex; + + align-items: center; + justify-content: flex-start; +} + +.checkbox { + flex: 0 0 auto; + margin: 0 10px 0 20px; +} + +.actionsList { + display: flex; + height: 100%; + flex: 0 0 auto; +} + +.action { + height: 100%; + box-sizing: border-box; + padding: 0 10px; + + color: inherit; + font: 500 13px/37px 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.action:disabled { + color: #ccc; + cursor: initial; +} + +.action:enabled:hover { + background: rgba(0, 0, 0, 0.05); +} diff --git a/src/components/actions-box/actions/action.jsx b/src/components/actions-box/actions/action.jsx new file mode 100644 index 0000000..854e075 --- /dev/null +++ b/src/components/actions-box/actions/action.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import { Button } from '../../button'; + +import styles from './action.module.css'; + +export const Action = ({ disabled, handleAction, children }) => ( + +); diff --git a/src/components/actions-box/actions/action.module.css b/src/components/actions-box/actions/action.module.css new file mode 100644 index 0000000..21fb0ec --- /dev/null +++ b/src/components/actions-box/actions/action.module.css @@ -0,0 +1,17 @@ +.action { + height: 100%; + box-sizing: border-box; + padding: 0 10px; + + color: inherit; + font: 500 13px/37px 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.action:disabled { + color: #ccc; + cursor: initial; +} + +.action:enabled:hover { + background: rgba(0, 0, 0, 0.05); +} diff --git a/src/components/actions-box/actions/delete-letters-action/delete-letters.jsx b/src/components/actions-box/actions/delete-letters-action/delete-letters.jsx new file mode 100644 index 0000000..cd317e8 --- /dev/null +++ b/src/components/actions-box/actions/delete-letters-action/delete-letters.jsx @@ -0,0 +1,21 @@ +import React, { useContext } from 'react'; +import { cb, mem } from '../../../../react-utils'; + +import { Action } from '../action'; +import { LettersContext } from '../../../letters-context'; + +export const DeleteLetterAction = props => { + const { getLetters, changeLetters } = useContext(LettersContext); + const letters = mem(() => getLetters()).deps(getLetters); + + const handleAction = cb(() => { + const ids = letters.filter(letter => letter.checked).map(letter => letter.id); + changeLetters(letter => (ids.includes(letter.id) ? { state: 3, checked: false } : {})); + }).deps(letters, changeLetters); + + return ( + + Удалить + + ); +}; diff --git a/src/components/actions-box/actions/delete-letters-action/index.js b/src/components/actions-box/actions/delete-letters-action/index.js new file mode 100644 index 0000000..7c66ce9 --- /dev/null +++ b/src/components/actions-box/actions/delete-letters-action/index.js @@ -0,0 +1 @@ +export * from './delete-letters'; diff --git a/src/components/actions-box/actions/index.js b/src/components/actions-box/actions/index.js new file mode 100644 index 0000000..2c0622f --- /dev/null +++ b/src/components/actions-box/actions/index.js @@ -0,0 +1 @@ +export * from './action'; diff --git a/src/components/actions-box/actions/read-letters-action/index.js b/src/components/actions-box/actions/read-letters-action/index.js new file mode 100644 index 0000000..787dcfe --- /dev/null +++ b/src/components/actions-box/actions/read-letters-action/index.js @@ -0,0 +1 @@ +export * from './read-letters'; diff --git a/src/components/actions-box/actions/read-letters-action/read-letters.jsx b/src/components/actions-box/actions/read-letters-action/read-letters.jsx new file mode 100644 index 0000000..59245d6 --- /dev/null +++ b/src/components/actions-box/actions/read-letters-action/read-letters.jsx @@ -0,0 +1,21 @@ +import React, { useContext } from 'react'; +import { cb, mem } from '../../../../react-utils'; + +import { Action } from '../action'; +import { LettersContext } from '../../../letters-context'; + +export const ReadLettersAction = props => { + const { getLetters, changeLetters } = useContext(LettersContext); + const letters = mem(() => getLetters()).deps(getLetters); + + const handleAction = cb(() => { + const ids = letters.filter(letter => letter.checked).map(letter => letter.id); + changeLetters(letter => (ids.includes(letter.id) ? { unread: false } : {})); + }).deps(letters, changeLetters); + + return ( + + Прочитано + + ); +}; diff --git a/src/components/actions-box/index.js b/src/components/actions-box/index.js new file mode 100644 index 0000000..7111ee3 --- /dev/null +++ b/src/components/actions-box/index.js @@ -0,0 +1 @@ +export * from './actions-box'; diff --git a/src/components/animation-wrapper/animation-wrapper.jsx b/src/components/animation-wrapper/animation-wrapper.jsx new file mode 100644 index 0000000..cd7f967 --- /dev/null +++ b/src/components/animation-wrapper/animation-wrapper.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { pure } from 'recompose'; + +export const AnimationWrapper = pure( + ({ animationClass, handleAnimationStart, handleAnimationEnd, children }) => { + return ( +
+ {children} +
+ ); + } +); diff --git a/src/components/animation-wrapper/index.js b/src/components/animation-wrapper/index.js new file mode 100644 index 0000000..2e4e9cb --- /dev/null +++ b/src/components/animation-wrapper/index.js @@ -0,0 +1 @@ +export * from './animation-wrapper'; diff --git a/src/components/app/app.jsx b/src/components/app/app.jsx new file mode 100644 index 0000000..466bce9 --- /dev/null +++ b/src/components/app/app.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; + +import { Header } from '../header'; +import { Content } from '../content'; +import { Sidebar } from '../sidebar'; +import { LettersProvider } from '../letters-context'; + +import styles from './app.module.css'; + +export const App = () => ( + + + + + +
+ +
+ + + +
+
+
+); diff --git a/src/components/app/app.module.css b/src/components/app/app.module.css new file mode 100644 index 0000000..13b09b1 --- /dev/null +++ b/src/components/app/app.module.css @@ -0,0 +1,37 @@ +.app { + display: grid; + + width: 100%; + min-width: 800px; + height: auto; + min-height: 100vh; + + box-sizing: border-box; + + padding: 12px 40px 30px 20px; + margin: 0; + + background-color: #e5eaf0; + + grid-column-gap: 25px; + grid-row-gap: 20px; + + grid-template-areas: + 'header header' + 'sidebar content'; + + grid-template-columns: 150px 1fr; + grid-template-rows: 32px minmax(400px, auto); +} + +.header { + grid-area: header; +} + +.sidebar { + grid-area: sidebar; +} + +.content { + grid-area: content; +} diff --git a/src/app/index.js b/src/components/app/index.js similarity index 100% rename from src/app/index.js rename to src/components/app/index.js diff --git a/src/components/button/button.jsx b/src/components/button/button.jsx new file mode 100644 index 0000000..f6ffd93 --- /dev/null +++ b/src/components/button/button.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import classNames from 'classnames'; + +import styles from './button.module.css'; + +export const Button = ({ className, handleClick, disabled, type, children }) => { + return ( + + ); +}; diff --git a/src/components/button/button.module.css b/src/components/button/button.module.css new file mode 100644 index 0000000..03658b8 --- /dev/null +++ b/src/components/button/button.module.css @@ -0,0 +1,7 @@ +.button { + padding: 0; + + border: none; + background: initial; + cursor: pointer; +} diff --git a/src/components/button/index.js b/src/components/button/index.js new file mode 100644 index 0000000..eaf5eea --- /dev/null +++ b/src/components/button/index.js @@ -0,0 +1 @@ +export * from './button'; diff --git a/src/components/checkbox/checkbox.jsx b/src/components/checkbox/checkbox.jsx new file mode 100644 index 0000000..a53b058 --- /dev/null +++ b/src/components/checkbox/checkbox.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { pure } from 'recompose'; + +import styles from './checkbox.module.css'; + +export const Checkbox = pure(({ className, id, handleChange, checked, disabled }) => { + return ( +
+ +
+ ); +}); diff --git a/src/components/checkbox/checkbox.module.css b/src/components/checkbox/checkbox.module.css new file mode 100644 index 0000000..d59ed4a --- /dev/null +++ b/src/components/checkbox/checkbox.module.css @@ -0,0 +1,68 @@ +.label { + position: relative; + + display: block; + + width: 16px; + margin: 0 auto; + + cursor: pointer; +} + +.checkbox_view { + display: block; + + width: 16px; + height: 16px; + + border: 1px solid #ccc; + background-color: #fff; + border-radius: 3px; +} + +.checkbox_view::after { + position: absolute; + + bottom: 4px; + left: 1px; + + width: 18px; + height: 18px; + + background-image: url(./checkbox.svg); + content: ''; + visibility: hidden; +} + +.checkbox_view:enabled:hover { + border-color: #999; +} + +.real_checkbox { + position: absolute; + appearance: none; +} + +.real_checkbox:active + .checkbox_view { + background-color: #f6f5f3; +} + +.real_checkbox:checked + .checkbox_view { + border-color: #c2b275; + background-color: #ffeba0; +} + +.real_checkbox:checked + .checkbox_view::after { + visibility: visible; +} + +.real_checkbox:enabled:focus + .checkbox_view { + border-color: #e4c23b; + box-shadow: 0 0 15px 1px #e4c23b; +} + +.real_checkbox:disabled + .checkbox_view { + background-color: #fff; + cursor: initial; + opacity: 0.3; +} diff --git a/src/components/checkbox/checkbox.svg b/src/components/checkbox/checkbox.svg new file mode 100644 index 0000000..df2781b --- /dev/null +++ b/src/components/checkbox/checkbox.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/checkbox/index.js b/src/components/checkbox/index.js new file mode 100644 index 0000000..8d78b3e --- /dev/null +++ b/src/components/checkbox/index.js @@ -0,0 +1 @@ +export * from './checkbox'; diff --git a/src/components/content/content.jsx b/src/components/content/content.jsx new file mode 100644 index 0000000..6070d61 --- /dev/null +++ b/src/components/content/content.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Route, Switch } from 'react-router-dom'; + +import { ActionsBox } from '../actions-box'; +import { Footer } from '../footer'; +import { Letters } from '../letters'; + +import styles from './content.module.css'; +import { LetterWindow } from '../letter-window'; + +export const Content = ({ className }) => { + return ( +
+ + + + + + ( + <> + + + + )} + /> + +
+
+ ); +}; diff --git a/src/components/content/content.module.css b/src/components/content/content.module.css new file mode 100644 index 0000000..10ddfe0 --- /dev/null +++ b/src/components/content/content.module.css @@ -0,0 +1,28 @@ +.main { + display: grid; + height: auto; + + background-color: #fff; + border-radius: 3px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.34); + + grid-template: 42px auto 35px / 1fr; + grid-template-areas: + 'actions' + 'content' + 'footer'; +} + +.actions { + border-bottom: 1px solid #e2e2e2; + grid-area: actions; +} + +.content { + grid-area: content; +} + +.footer { + border-top: 1px solid #e2e2e2; + grid-area: footer; +} diff --git a/src/components/content/index.js b/src/components/content/index.js new file mode 100644 index 0000000..7b367d1 --- /dev/null +++ b/src/components/content/index.js @@ -0,0 +1 @@ +export * from './content'; diff --git a/src/components/date/date.jsx b/src/components/date/date.jsx new file mode 100644 index 0000000..6635f1a --- /dev/null +++ b/src/components/date/date.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { pure } from 'recompose'; +import classNames from 'classnames'; +import { formatDatetime, toRussianDate } from '../../utils'; +import styles from './date.module.css'; + +export const Date = pure(({ date, className }) => ( + +)); diff --git a/src/components/date/date.module.css b/src/components/date/date.module.css new file mode 100644 index 0000000..e65b320 --- /dev/null +++ b/src/components/date/date.module.css @@ -0,0 +1,4 @@ +.date { + color: #9b9b9b; + font: 13px 'Helvetica Neue', Arial, sans-serif; +} diff --git a/src/components/date/index.js b/src/components/date/index.js new file mode 100644 index 0000000..05b562f --- /dev/null +++ b/src/components/date/index.js @@ -0,0 +1 @@ +export * from './date'; diff --git a/src/components/footer/footer.jsx b/src/components/footer/footer.jsx new file mode 100644 index 0000000..c6cb2b8 --- /dev/null +++ b/src/components/footer/footer.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { Link } from '../link'; + +import styles from './footer.module.css'; + +export const Footer = ({ className }) => { + return ( + + ); +}; diff --git a/src/components/footer/footer.module.css b/src/components/footer/footer.module.css new file mode 100644 index 0000000..3535edf --- /dev/null +++ b/src/components/footer/footer.module.css @@ -0,0 +1,14 @@ +.footer { + display: flex; + height: 100%; + + align-items: center; + justify-content: flex-end; +} + +.item { + margin-right: 30px; + + color: #9b9b9b; + font: 11px/31px 'Helvetica Neue', Helvetica, Arial, sans-serif; +} diff --git a/src/components/footer/index.js b/src/components/footer/index.js new file mode 100644 index 0000000..a058eae --- /dev/null +++ b/src/components/footer/index.js @@ -0,0 +1 @@ +export * from './footer'; diff --git a/src/components/header/hamburger.svg b/src/components/header/hamburger.svg new file mode 100644 index 0000000..d783ed7 --- /dev/null +++ b/src/components/header/hamburger.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/header/header.jsx b/src/components/header/header.jsx new file mode 100644 index 0000000..d849457 --- /dev/null +++ b/src/components/header/header.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { Search } from '../search'; +import { Button } from '../button/button'; +import { Link } from '../link'; + +import { ReactComponent as HamburgerIcon } from './hamburger.svg'; +import { ReactComponent as YandexMailIcon } from './mail-yandex.svg'; + +import styles from './header.module.css'; + +export const Header = ({ className }) => { + return ( +
+ + + + + +
+ ); +}; diff --git a/src/components/header/header.module.css b/src/components/header/header.module.css new file mode 100644 index 0000000..e536fc0 --- /dev/null +++ b/src/components/header/header.module.css @@ -0,0 +1,25 @@ +.header { + display: grid; + align-items: center; + grid-template: 100% / auto 12px auto 1fr; + grid-template-areas: 'hamburger . logo search'; + justify-items: center; +} + +.hamburger { + grid-area: hamburger; +} + +.logo { + grid-area: logo; +} + +.search { + width: 40%; + min-width: 300px; + max-width: 600px; + + margin: 0 60px; + + grid-area: search; +} diff --git a/src/components/header/image.svg b/src/components/header/image.svg new file mode 100644 index 0000000..03c806b --- /dev/null +++ b/src/components/header/image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/header/index.js b/src/components/header/index.js new file mode 100644 index 0000000..677ca79 --- /dev/null +++ b/src/components/header/index.js @@ -0,0 +1 @@ +export * from './header'; diff --git a/src/components/header/mail-yandex.svg b/src/components/header/mail-yandex.svg new file mode 100644 index 0000000..aebfa92 --- /dev/null +++ b/src/components/header/mail-yandex.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/letter-window/index.js b/src/components/letter-window/index.js new file mode 100644 index 0000000..7583f3b --- /dev/null +++ b/src/components/letter-window/index.js @@ -0,0 +1 @@ +export * from './letter-window'; diff --git a/src/components/letter-window/letter-window.jsx b/src/components/letter-window/letter-window.jsx new file mode 100644 index 0000000..06bd993 --- /dev/null +++ b/src/components/letter-window/letter-window.jsx @@ -0,0 +1,37 @@ +import React, { useContext } from 'react'; +import classNames from 'classnames'; +import { mem } from '../../react-utils'; + +import { LettersContext } from '../letters-context'; +import { Text } from '../text'; +import { ProfileImage } from '../profile-image'; +import { Date } from '../date'; + +import style from './letter-window.module.css'; + +export const LetterWindow = ({ className, id }) => { + if (!Number.isInteger(+id)) { + return null; + } + + const { getLetterByID } = useContext(LettersContext); + const letter = mem(() => getLetterByID(parseInt(id, 10))).deps(getLetterByID); + + return ( +
+
+ + {letter.content.content.subject} + +
+ + + {letter.content.user.name} + + +
+
+
{letter.content.content.body}
+
+ ); +}; diff --git a/src/components/letter-window/letter-window.module.css b/src/components/letter-window/letter-window.module.css new file mode 100644 index 0000000..8f9d10d --- /dev/null +++ b/src/components/letter-window/letter-window.module.css @@ -0,0 +1,56 @@ +.content { + width: 100%; + min-width: 700px; + max-width: calc(100vw - 250px); + height: auto; + box-sizing: border-box; + padding: 0 15%; +} + +.body { + width: 100%; + height: auto; + + box-sizing: border-box; + padding-bottom: 30px; + + font: 16px/30px 'Helvetica Neue', Helvetica, Arial, sans-serif; + text-align: justify; +} + +.header { + width: 100%; + height: auto; + + margin-bottom: 20px; +} + +.subject { + width: 100%; + font: 400 24px/80px 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.profileImage { + width: 50px; + height: 50px; + grid-area: image; +} + +.messageInfo { + display: grid; + + grid-column-gap: 20px; + grid-template-areas: 'image name date'; + grid-template-columns: 50px 300px 1fr; + grid-template-rows: 50px; +} + +.name { + font: 700 16px 'Helvetica Neue', Helvetica, Arial, sans-serif; + grid-area: name; +} + +.date { + grid-area: date; + justify-self: end; +} diff --git a/src/components/letter/deleted/deleted-letter.jsx b/src/components/letter/deleted/deleted-letter.jsx new file mode 100644 index 0000000..6f57fef --- /dev/null +++ b/src/components/letter/deleted/deleted-letter.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { pure } from 'recompose'; + +import { Letter } from '../letter'; +import styles from './deleted-letter.module.css'; +import { AnimationWrapper } from '../../animation-wrapper'; + +export const DeletedLetter = pure(props => { + return ( + + + + ); +}); diff --git a/src/components/letter/deleted/deleted-letter.module.css b/src/components/letter/deleted/deleted-letter.module.css new file mode 100644 index 0000000..ad1bb37 --- /dev/null +++ b/src/components/letter/deleted/deleted-letter.module.css @@ -0,0 +1,20 @@ +.deletedLetter { + animation: delete-letter-animation 0.6s cubic-bezier(0.5, 0, 0, 0); +} + +@keyframes delete-letter-animation { + 0% { + height: 41px; + opacity: 1; + } + + 50% { + height: 41px; + opacity: 0; + } + + 100% { + height: 0; + opacity: 0; + } +} diff --git a/src/components/letter/deleted/index.js b/src/components/letter/deleted/index.js new file mode 100644 index 0000000..5144c17 --- /dev/null +++ b/src/components/letter/deleted/index.js @@ -0,0 +1 @@ +export * from './deleted-letter'; diff --git a/src/components/letter/index.js b/src/components/letter/index.js new file mode 100644 index 0000000..c37f80d --- /dev/null +++ b/src/components/letter/index.js @@ -0,0 +1 @@ +export * from './letter'; diff --git a/src/components/letter/letter.jsx b/src/components/letter/letter.jsx new file mode 100644 index 0000000..8da0661 --- /dev/null +++ b/src/components/letter/letter.jsx @@ -0,0 +1,50 @@ +import React, { useContext } from 'react'; +import classNames from 'classnames/bind'; +import { pure } from 'recompose'; +import { Checkbox } from '../checkbox'; +import { ProfileImage } from '../profile-image'; +import { Date } from '../date'; +import { UnreadFlag } from './unread-flag'; +import { Link } from '../link'; +import { Text } from '../text'; +import { LettersContext } from '../letters-context'; + +import styles from './letter.module.css'; + +const cx = classNames.bind(styles); + +export const Letter = pure(({ id, unread, checked, content }) => { + const { changeLetterByID } = useContext(LettersContext); + + const handleChange = event => { + const targetChecked = event.target.checked; + if (checked !== targetChecked) { + changeLetterByID(id, () => ({ checked: targetChecked })); + } + }; + + const handleClick = () => { + changeLetterByID(id, () => ({ unread: false })); + }; + + return ( + +
+ + + + {content.user.name} + + + + {content.content.subject} + + +
+ + ); +}); diff --git a/src/components/letter/letter.module.css b/src/components/letter/letter.module.css new file mode 100644 index 0000000..9fb221e --- /dev/null +++ b/src/components/letter/letter.module.css @@ -0,0 +1,76 @@ +.letter { + display: grid; + + align-items: center; + justify-content: start; + + padding: 0 20px; + + border-bottom: 1px solid #e2e2e2; + column-gap: 15px; + grid-template: 40px / 16px 30px 165px 10px 1fr 60px; +} + +.letterActive { + background-color: rgba(0, 0, 0, 0.05); +} + +.deletedLetter { + animation: delete-letter-animation 1s cubic-bezier(0.5, 0, 0, 0); +} + +@keyframes delete-letter-animation { + 0% { + height: 41px; + opacity: 1; + } + + 30% { + height: 41px; + opacity: 0; + } + + 100% { + height: 0; + opacity: 0; + } +} + +.newLetter { + animation: new-letter-animation 1s cubic-bezier(1, 1, 0.5, 1); +} + +@keyframes new-letter-animation { + 0% { + height: 0; + opacity: 0; + } + + 50% { + height: 40px; + opacity: 0; + } + + 100% { + height: 40px; + opacity: 1; + } +} + +.link { + display: block; + height: auto; +} + +.text { + color: #000; + font: 13px 'Helvetica Neue', Arial, sans-serif; +} + +.textUnread { + font-weight: bold; +} + +.date { + justify-self: end; +} diff --git a/src/components/letter/new/index.js b/src/components/letter/new/index.js new file mode 100644 index 0000000..9b16edb --- /dev/null +++ b/src/components/letter/new/index.js @@ -0,0 +1 @@ +export * from './new-letter'; diff --git a/src/components/letter/new/new-letter.jsx b/src/components/letter/new/new-letter.jsx new file mode 100644 index 0000000..abb7b48 --- /dev/null +++ b/src/components/letter/new/new-letter.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { pure } from 'recompose'; +import { Letter } from '../letter'; +import { AnimationWrapper } from '../../animation-wrapper'; + +import styles from './new-letter.module.css'; + +export const NewLetter = pure(props => { + return ( + + + + ); +}); diff --git a/src/components/letter/new/new-letter.module.css b/src/components/letter/new/new-letter.module.css new file mode 100644 index 0000000..dfca664 --- /dev/null +++ b/src/components/letter/new/new-letter.module.css @@ -0,0 +1,20 @@ +.newLetter { + animation: new-letter-animation 0.6s cubic-bezier(1, 1, 0.5, 1); +} + +@keyframes new-letter-animation { + 0% { + height: 0; + opacity: 0; + } + + 50% { + height: 41px; + opacity: 0; + } + + 100% { + height: 41px; + opacity: 1; + } +} diff --git a/src/components/letter/unread-flag/index.js b/src/components/letter/unread-flag/index.js new file mode 100644 index 0000000..bec34bd --- /dev/null +++ b/src/components/letter/unread-flag/index.js @@ -0,0 +1 @@ +export * from './unread-flag'; diff --git a/src/components/letter/unread-flag/unread-flag.jsx b/src/components/letter/unread-flag/unread-flag.jsx new file mode 100644 index 0000000..b457ca6 --- /dev/null +++ b/src/components/letter/unread-flag/unread-flag.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { pure } from 'recompose'; +import classNames from 'classnames/bind'; + +import styles from './unread-flag.module.css'; + +const cx = classNames.bind(styles); + +export const UnreadFlag = pure(({ visible }) =>
); diff --git a/src/components/letter/unread-flag/unread-flag.module.css b/src/components/letter/unread-flag/unread-flag.module.css new file mode 100644 index 0000000..a8d5424 --- /dev/null +++ b/src/components/letter/unread-flag/unread-flag.module.css @@ -0,0 +1,11 @@ +.flag { + width: 10px; + height: 10px; + + background-color: #6287bd; + border-radius: 50%; +} + +.hidden { + visibility: hidden; +} diff --git a/src/components/letters-context/index.js b/src/components/letters-context/index.js new file mode 100644 index 0000000..20832f9 --- /dev/null +++ b/src/components/letters-context/index.js @@ -0,0 +1 @@ +export * from './letters-context'; diff --git a/src/components/letters-context/letters-context.jsx b/src/components/letters-context/letters-context.jsx new file mode 100644 index 0000000..3ae4888 --- /dev/null +++ b/src/components/letters-context/letters-context.jsx @@ -0,0 +1,83 @@ +import React, { createContext } from 'react'; +import { useSessionStorage, cb, mem } from '../../react-utils'; +import { mergeObjects } from '../../utils'; + +export const LetterState = Object.freeze({ + NEW: 1, + COMMON: 2, + WILL_BE_DELETED: 3 +}); + +const createLetter = (letterContent, id) => ({ + id, + state: LetterState.NEW, + unread: true, + checked: false, + content: letterContent +}); + +export const LettersContext = createContext(null); + +export const LettersProvider = ({ children }) => { + const [state, setState] = useSessionStorage('letters', { counter: 0, letters: [] }); + + const getLetters = cb(() => state.letters).deps(state); + + const getLetterByID = cb(id => getLetters().find(letter => letter.id === id)).deps(getLetters); + + const changeState = cb((counterFunc, lettersFunc) => + setState(({ counter, letters }) => ({ + counter: counterFunc(counter, letters), + letters: lettersFunc(letters, counter) + })) + ).deps(setState); + + const setLetters = cb(lettersFunc => changeState(counter => counter, lettersFunc)).deps( + changeState + ); + + const filterLetters = cb(pred => setLetters(letters => letters.filter(pred))).deps(setLetters); + + const mapLetters = cb(func => + setLetters(letters => letters.map(letter => mergeObjects(letter, func(letter)))) + ).deps(setLetters); + + const addLetter = cb(letterContent => + changeState( + counter => counter + 1, + (letters, counter) => [createLetter(letterContent, counter), ...letters] + ) + ).deps(setLetters, createLetter); + + const deleteLetterByID = cb(id => filterLetters(letter => letter.id !== id)).deps(filterLetters); + + const deleteLettersByIDs = cb(ids => filterLetters(letter => !ids.includes(letter.id))).deps( + filterLetters + ); + + const changeLetterByID = cb((id, func) => + mapLetters(letter => (letter.id === id ? func(letter) : letter)) + ).deps(mapLetters); + + const changeLetters = cb(func => mapLetters(func)).deps(mapLetters); + + const contextValue = mem(() => ({ + getLetters, + getLetterByID, + addLetter, + deleteLetterByID, + deleteLettersByIDs, + changeLetterByID, + changeLetters + })).deps( + getLetters, + getLetterByID, + addLetter, + deleteLetterByID, + deleteLettersByIDs, + changeLetterByID, + changeLetters + ); + + return {children}; +}; diff --git a/src/components/letters-page/index.js b/src/components/letters-page/index.js new file mode 100644 index 0000000..ce7fe19 --- /dev/null +++ b/src/components/letters-page/index.js @@ -0,0 +1 @@ +export * from './letterss-page-container'; diff --git a/src/components/letters-page/letters-page-component/index.js b/src/components/letters-page/letters-page-component/index.js new file mode 100644 index 0000000..6dccf2b --- /dev/null +++ b/src/components/letters-page/letters-page-component/index.js @@ -0,0 +1 @@ +export * from './letters-page-component'; diff --git a/src/components/letters-page/letters-page-component/letters-list/index.js b/src/components/letters-page/letters-page-component/letters-list/index.js new file mode 100644 index 0000000..f619923 --- /dev/null +++ b/src/components/letters-page/letters-page-component/letters-list/index.js @@ -0,0 +1 @@ +export * from './letters-list'; diff --git a/src/components/letters-page/letters-page-component/letters-list/letters-list.jsx b/src/components/letters-page/letters-page-component/letters-list/letters-list.jsx new file mode 100644 index 0000000..09be429 --- /dev/null +++ b/src/components/letters-page/letters-page-component/letters-list/letters-list.jsx @@ -0,0 +1,21 @@ +import React, { Component } from 'react'; + +import { Letter } from '../../../letter'; + +import styles from './letters-list.module.css'; + +export class LettersList extends Component { + render() { + return ( +
+
    + {this.props.letters.map(letter => ( +
  • + +
  • + ))} +
+
+ ); + } +} diff --git a/src/components/letters-page/letters-page-component/letters-list/letters-list.module.css b/src/components/letters-page/letters-page-component/letters-list/letters-list.module.css new file mode 100644 index 0000000..cd34408 --- /dev/null +++ b/src/components/letters-page/letters-page-component/letters-list/letters-list.module.css @@ -0,0 +1,10 @@ +.list { + padding: 0; + margin: 0; + list-style-type: none; +} + +.letter { + box-sizing: border-box; + padding: 0 20px; +} diff --git a/src/components/letters-page/letters-page-component/letters-page-component.jsx b/src/components/letters-page/letters-page-component/letters-page-component.jsx new file mode 100644 index 0000000..e590c7f --- /dev/null +++ b/src/components/letters-page/letters-page-component/letters-page-component.jsx @@ -0,0 +1,19 @@ +import React, { Component } from 'react'; + +import { ActionsBox } from '../../actions-box'; +import { Footer } from '../../footer'; +import { LettersList } from './letters-list'; + +import styles from './letters-page.module.css'; + +export class LettersPageComponent extends Component { + render() { + return ( +
+ + +
+
+ ); + } +} diff --git a/src/components/letters-page/letters-page-component/letters-page.module.css b/src/components/letters-page/letters-page-component/letters-page.module.css new file mode 100644 index 0000000..7456347 --- /dev/null +++ b/src/components/letters-page/letters-page-component/letters-page.module.css @@ -0,0 +1,28 @@ +.letters_page { + display: flex; + min-width: 600px; + flex-direction: column; + + background-color: #fff; + + border-radius: 3px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.34); +} + +.actions_box { + box-sizing: border-box; + padding: 0 20px; + + border-bottom: 1px solid #e2e2e2; +} + +.list { + min-height: calc(100vh - 200px); +} + +.footer { + box-sizing: border-box; + padding: 0 20px; + + border-top: 1px solid #e2e2e2; +} diff --git a/src/components/letters-page/letterss-page-container.jsx b/src/components/letters-page/letterss-page-container.jsx new file mode 100644 index 0000000..35d7698 --- /dev/null +++ b/src/components/letters-page/letterss-page-container.jsx @@ -0,0 +1,36 @@ +import React, { Component } from 'react'; + +import { LettersPageComponent } from './letters-page-component'; + +export class LettersPageContainer extends Component { + constructor(props) { + super(props); + this.state = { + letters: [ + { + id: '0', + unread: true, + user: { + name: 'Яндекс.Почта', + imageUrl: 'https://thispersondoesnotexist.com/image' + }, + subject: 'Как читать почту с мобильного', + date: new Date() + }, + { + id: '1', + unread: false, + user: { + name: 'Вася Петров' + }, + subject: 'Как не читать почту не с мобильного', + date: new Date() + } + ] + }; + } + + render() { + return ; + } +} diff --git a/src/components/letters/index.js b/src/components/letters/index.js new file mode 100644 index 0000000..de36da6 --- /dev/null +++ b/src/components/letters/index.js @@ -0,0 +1 @@ +export * from './letters'; diff --git a/src/components/letters/letters.jsx b/src/components/letters/letters.jsx new file mode 100644 index 0000000..80c6a7f --- /dev/null +++ b/src/components/letters/letters.jsx @@ -0,0 +1,65 @@ +import React, { Fragment, useContext } from 'react'; +import { mem } from '../../react-utils'; +import { createLatch } from '../../utils'; + +import { List } from '../list'; +import { Letter } from '../letter'; +import { NewLetter } from '../letter/new'; +import { DeletedLetter } from '../letter/deleted'; +import { LettersContext, LetterState } from '../letters-context'; + +export const Letters = ({ className }) => { + const { getLetters, changeLetterByID, deleteLettersByIDs } = useContext(LettersContext); + const letters = mem(() => getLetters()).deps(getLetters); + + const deletingLettersIDs = mem(() => + letters.filter(letter => letter.state === 3).map(letter => letter.id) + ).deps(letters); + + const latch = createLatch(deletingLettersIDs.length, () => + deleteLettersByIDs(deletingLettersIDs) + ); + + const letterProps = letter => ({ + id: letter.id, + unread: letter.unread, + checked: letter.checked, + content: letter.content + }); + + const commonLetterComponent = letter => ; + + const newLetterComponent = letter => ( + changeLetterByID(letter.id, () => ({ state: 2 }))} + {...letterProps(letter)} + /> + ); + + const deletedLetterComponent = letter => ( + latch.countDown()} {...letterProps(letter)} /> + ); + + const letterComponent = letter => { + switch (letter.state) { + case LetterState.NEW: + return newLetterComponent(letter); + case LetterState.COMMON: + return commonLetterComponent(letter); + case LetterState.WILL_BE_DELETED: + return deletedLetterComponent(letter); + default: + return null; + } + }; + + const items = mem(() => + letters.map(letter => {letterComponent(letter)}) + ).deps(letters, letterComponent); + + return ( +
+ {items} +
+ ); +}; diff --git a/src/components/letters/letters.module.css b/src/components/letters/letters.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/link/index.js b/src/components/link/index.js new file mode 100644 index 0000000..e33728e --- /dev/null +++ b/src/components/link/index.js @@ -0,0 +1 @@ +export * from './link'; diff --git a/src/components/link/link.jsx b/src/components/link/link.jsx new file mode 100644 index 0000000..0720b6f --- /dev/null +++ b/src/components/link/link.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { pure } from 'recompose'; +import classNames from 'classnames'; + +import styles from './link.module.css'; + +export const Link = pure(props => { + return ( + + {props.children} + + ); +}); diff --git a/src/components/link/link.module.css b/src/components/link/link.module.css new file mode 100644 index 0000000..d5960b4 --- /dev/null +++ b/src/components/link/link.module.css @@ -0,0 +1,3 @@ +.link { + text-decoration: none; +} diff --git a/src/components/list/index.js b/src/components/list/index.js new file mode 100644 index 0000000..7182513 --- /dev/null +++ b/src/components/list/index.js @@ -0,0 +1 @@ +export * from './list'; diff --git a/src/components/list/list.jsx b/src/components/list/list.jsx new file mode 100644 index 0000000..30dfa11 --- /dev/null +++ b/src/components/list/list.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import classNames from 'classnames'; + +import styles from './list.module.css'; + +export const List = ({ className, itemClassName, children }) => { + return ( +
    + {React.Children.map(children, child => ( +
  • {child}
  • + ))} +
+ ); +}; diff --git a/src/components/list/list.module.css b/src/components/list/list.module.css new file mode 100644 index 0000000..8d73171 --- /dev/null +++ b/src/components/list/list.module.css @@ -0,0 +1,6 @@ +.list { + padding: 0; + margin: 0; + + list-style-type: none; +} diff --git a/src/components/nav/index.js b/src/components/nav/index.js new file mode 100644 index 0000000..38d6745 --- /dev/null +++ b/src/components/nav/index.js @@ -0,0 +1 @@ +export * from './nav'; diff --git a/src/components/nav/nav.jsx b/src/components/nav/nav.jsx new file mode 100644 index 0000000..bc4a479 --- /dev/null +++ b/src/components/nav/nav.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import classNames from 'classnames/bind'; + +import { List } from '../list'; +import { Link } from '../link'; +import { Button } from '../button'; +import { Text } from '../text'; + +import styles from './nav.module.css'; + +const cx = classNames.bind(styles); + +export const Nav = ({ className }) => { + return ( + + ); +}; + +const LinkItem = ({ href, isActive, children }) => ( + + {children} + +); + +const ButtonItem = ({ children }) => ( + +); diff --git a/src/components/nav/nav.module.css b/src/components/nav/nav.module.css new file mode 100644 index 0000000..7443fc0 --- /dev/null +++ b/src/components/nav/nav.module.css @@ -0,0 +1,33 @@ +.list { + width: 100%; +} + +.item { + display: block; + width: 100%; + height: 22px; + box-sizing: border-box; +} + +.clickable { + display: block; + width: 100%; + height: 100%; + + box-sizing: border-box; + padding: 0 10px; + + color: #707070; + + font: 500 11px/22px 'Helvetica Neue', Helvetica, Arial, sans-serif; + text-align: left; +} + +.active, +.clickable:hover { + background-color: #cdd6e4; + border-radius: 3px; + + color: #555; + font-weight: bold; +} diff --git a/src/components/profile-image/index.js b/src/components/profile-image/index.js new file mode 100644 index 0000000..44545fa --- /dev/null +++ b/src/components/profile-image/index.js @@ -0,0 +1 @@ +export * from './profile-image'; diff --git a/src/components/profile-image/profile-image.jsx b/src/components/profile-image/profile-image.jsx new file mode 100644 index 0000000..59dba30 --- /dev/null +++ b/src/components/profile-image/profile-image.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import classNames from 'classnames'; +import { pure } from 'recompose'; +import { initials, randomColor } from '../../utils'; + +import styles from './profile-image.module.css'; + +export const ProfileImage = pure(({ className, imageUrl, name }) => { + return imageUrl ? ( + + ) : ( + + ); +}); + +const ProfileImageComponent = pure(({ className, style, text }) => ( +
+ {text} +
+)); + +const WithInitials = pure(({ className, name }) => ( + +)); + +const WithImage = pure(({ className, imageUrl }) => ( + +)); diff --git a/src/components/profile-image/profile-image.module.css b/src/components/profile-image/profile-image.module.css new file mode 100644 index 0000000..06d98ba --- /dev/null +++ b/src/components/profile-image/profile-image.module.css @@ -0,0 +1,16 @@ +.profileImage { + display: flex; + + width: 30px; + height: 30px; + + align-items: center; + justify-content: center; + + background-color: #9b9b9b; + background-size: contain; + border-radius: 50%; + + color: #fff; + font: 500 16px/35px 'Yandex Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} diff --git a/src/components/profile-name/index.js b/src/components/profile-name/index.js new file mode 100644 index 0000000..013d4b9 --- /dev/null +++ b/src/components/profile-name/index.js @@ -0,0 +1 @@ +export * from './profile-name'; diff --git a/src/components/profile-name/profile-name.jsx b/src/components/profile-name/profile-name.jsx new file mode 100644 index 0000000..b48b899 --- /dev/null +++ b/src/components/profile-name/profile-name.jsx @@ -0,0 +1,7 @@ +import React, { Component } from 'react'; + +export class ProfileName extends Component { + render() { + return (); + } +} diff --git a/src/components/search/cross-icon.svg b/src/components/search/cross-icon.svg new file mode 100644 index 0000000..013d99f --- /dev/null +++ b/src/components/search/cross-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/search/index.js b/src/components/search/index.js new file mode 100644 index 0000000..5a2bdeb --- /dev/null +++ b/src/components/search/index.js @@ -0,0 +1 @@ +export * from './search'; diff --git a/src/components/search/search.jsx b/src/components/search/search.jsx new file mode 100644 index 0000000..c9fa471 --- /dev/null +++ b/src/components/search/search.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { Button } from '../button'; + +import { ReactComponent as CrossIcon } from './cross-icon.svg'; + +import styles from './search.module.css'; + +export const Search = ({ className }) => { + return ( +
+ + +
+ ); +}; diff --git a/src/components/search/search.module.css b/src/components/search/search.module.css new file mode 100644 index 0000000..bd26ab8 --- /dev/null +++ b/src/components/search/search.module.css @@ -0,0 +1,26 @@ +.search { + position: relative; +} + +.input { + width: 100%; + + box-sizing: border-box; + padding-right: 30px; + padding-left: 15px; + + border: 1px solid rgba(0, 0, 0, 0.2); + + font: 15px/32px 'Yandex Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + opacity: 0.5; +} + +.clearIcon { + position: absolute; + + top: calc(50% - 5px); + right: 15px; + + width: 10px; + height: 10px; +} diff --git a/src/components/show-letter-context/index.js b/src/components/show-letter-context/index.js new file mode 100644 index 0000000..730334a --- /dev/null +++ b/src/components/show-letter-context/index.js @@ -0,0 +1 @@ +export * from './show-letter-context'; diff --git a/src/components/show-letter-context/show-letter-context.jsx b/src/components/show-letter-context/show-letter-context.jsx new file mode 100644 index 0000000..c2b8fa8 --- /dev/null +++ b/src/components/show-letter-context/show-letter-context.jsx @@ -0,0 +1,20 @@ +import React, { createContext, useState, useCallback, useMemo } from 'react'; + +export const ShowLetterContext = createContext(null); + +export const ShowLetterProvider = ({ children }) => { + const [state, setState] = useState({ isShowing: false, letterId: -1 }); + + const showLetter = useCallback(id => setState(() => ({ isShowing: true, letterId: id })), [ + setState + ]); + + const getShowingState = useCallback(() => state, [state]); + + const contextValue = useMemo(() => ({ showLetter, getShowingState }), [ + showLetter, + getShowingState + ]); + + return {children}; +}; diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js new file mode 100644 index 0000000..b2ba9a4 --- /dev/null +++ b/src/components/sidebar/index.js @@ -0,0 +1 @@ +export * from './sidebar'; diff --git a/src/components/sidebar/sidebar.jsx b/src/components/sidebar/sidebar.jsx new file mode 100644 index 0000000..aa0d490 --- /dev/null +++ b/src/components/sidebar/sidebar.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { WriteButton } from '../write-button'; +import { Nav } from '../nav'; + +import styles from './sidebar.module.css'; + +export const Sidebar = ({ className }) => { + return ( + + ); +}; diff --git a/src/components/sidebar/sidebar.module.css b/src/components/sidebar/sidebar.module.css new file mode 100644 index 0000000..f2566e7 --- /dev/null +++ b/src/components/sidebar/sidebar.module.css @@ -0,0 +1,14 @@ +.sidebar { + width: 100%; +} + +.button { + width: 100%; + height: 32px; + + margin-bottom: 10px; +} + +.nav { + width: 100%; +} diff --git a/src/components/text/index.js b/src/components/text/index.js new file mode 100644 index 0000000..1a9ac14 --- /dev/null +++ b/src/components/text/index.js @@ -0,0 +1 @@ +export * from './text'; diff --git a/src/components/text/text.jsx b/src/components/text/text.jsx new file mode 100644 index 0000000..3809070 --- /dev/null +++ b/src/components/text/text.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { pure } from 'recompose'; +import classNames from 'classnames/bind'; + +import styles from './text.module.css'; + +const cx = classNames.bind(styles); + +export const Text = pure(({ className, withOverflow, children }) => { + return {children}; +}); diff --git a/src/components/text/text.module.css b/src/components/text/text.module.css new file mode 100644 index 0000000..88e5363 --- /dev/null +++ b/src/components/text/text.module.css @@ -0,0 +1,6 @@ +.withOverflowEllipsis { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/components/write-button/index.js b/src/components/write-button/index.js new file mode 100644 index 0000000..e5e2d02 --- /dev/null +++ b/src/components/write-button/index.js @@ -0,0 +1 @@ +export * from './write-button'; diff --git a/src/components/write-button/write-button.jsx b/src/components/write-button/write-button.jsx new file mode 100644 index 0000000..4258148 --- /dev/null +++ b/src/components/write-button/write-button.jsx @@ -0,0 +1,20 @@ +import React, { useContext } from 'react'; +import classNames from 'classnames'; + +import { Button } from '../button'; +import { generateLetter } from '../../utils/generators'; +import { LettersContext } from '../letters-context'; + +import styles from './write-button.module.css'; + +export const WriteButton = ({ className, children }) => { + const { addLetter } = useContext(LettersContext); + + const addNewLetter = () => addLetter(generateLetter()); + + return ( + + ); +}; diff --git a/src/components/write-button/write-button.module.css b/src/components/write-button/write-button.module.css new file mode 100644 index 0000000..9fbcd6b --- /dev/null +++ b/src/components/write-button/write-button.module.css @@ -0,0 +1,9 @@ +.writeButton { + display: block; + + background-color: #6287bd; + border-radius: 3px; + color: #fff; + + font: 500 12px/32px 'Helvetica Neue', Helvetica, Arial, sans-serif; +} diff --git a/src/fonts/HelveticaNeue-Bold.ttf b/src/fonts/HelveticaNeue-Bold.ttf new file mode 100644 index 0000000..c34caac Binary files /dev/null and b/src/fonts/HelveticaNeue-Bold.ttf differ diff --git a/src/fonts/HelveticaNeue-Bold.woff b/src/fonts/HelveticaNeue-Bold.woff new file mode 100644 index 0000000..eebe59b Binary files /dev/null and b/src/fonts/HelveticaNeue-Bold.woff differ diff --git a/src/fonts/HelveticaNeue-Medium.ttf b/src/fonts/HelveticaNeue-Medium.ttf new file mode 100644 index 0000000..94184ff Binary files /dev/null and b/src/fonts/HelveticaNeue-Medium.ttf differ diff --git a/src/fonts/HelveticaNeue-Medium.woff b/src/fonts/HelveticaNeue-Medium.woff new file mode 100644 index 0000000..7254e7d Binary files /dev/null and b/src/fonts/HelveticaNeue-Medium.woff differ diff --git a/src/fonts/HelveticaNeue.ttf b/src/fonts/HelveticaNeue.ttf new file mode 100644 index 0000000..e2cdb82 Binary files /dev/null and b/src/fonts/HelveticaNeue.ttf differ diff --git a/src/fonts/HelveticaNeue.woff b/src/fonts/HelveticaNeue.woff new file mode 100644 index 0000000..fc87b47 Binary files /dev/null and b/src/fonts/HelveticaNeue.woff differ diff --git a/src/fonts/YandexSansText-Bold.ttf b/src/fonts/YandexSansText-Bold.ttf new file mode 100644 index 0000000..ae1e5b3 Binary files /dev/null and b/src/fonts/YandexSansText-Bold.ttf differ diff --git a/src/fonts/YandexSansText-Bold.woff b/src/fonts/YandexSansText-Bold.woff new file mode 100644 index 0000000..9d3c7a2 Binary files /dev/null and b/src/fonts/YandexSansText-Bold.woff differ diff --git a/src/fonts/YandexSansText-Light.ttf b/src/fonts/YandexSansText-Light.ttf new file mode 100644 index 0000000..ce58a0c Binary files /dev/null and b/src/fonts/YandexSansText-Light.ttf differ diff --git a/src/fonts/YandexSansText-Light.woff b/src/fonts/YandexSansText-Light.woff new file mode 100644 index 0000000..28463ad Binary files /dev/null and b/src/fonts/YandexSansText-Light.woff differ diff --git a/src/fonts/YandexSansText-Medium.ttf b/src/fonts/YandexSansText-Medium.ttf new file mode 100644 index 0000000..df7d4a6 Binary files /dev/null and b/src/fonts/YandexSansText-Medium.ttf differ diff --git a/src/fonts/YandexSansText-Medium.woff b/src/fonts/YandexSansText-Medium.woff new file mode 100644 index 0000000..a171e3e Binary files /dev/null and b/src/fonts/YandexSansText-Medium.woff differ diff --git a/src/fonts/YandexSansText-Regular.ttf b/src/fonts/YandexSansText-Regular.ttf new file mode 100644 index 0000000..f9ef1ff Binary files /dev/null and b/src/fonts/YandexSansText-Regular.ttf differ diff --git a/src/fonts/YandexSansText-Regular.woff b/src/fonts/YandexSansText-Regular.woff new file mode 100644 index 0000000..ecaf598 Binary files /dev/null and b/src/fonts/YandexSansText-Regular.woff differ diff --git a/src/index.css b/src/index.css index 2b6e525..f916fac 100644 --- a/src/index.css +++ b/src/index.css @@ -1,10 +1,54 @@ +@font-face { + font-family: Yandex Sans; + font-weight: 300; + src: url(./fonts/YandexSansText-Light.woff) format('woff'), + url(./fonts/YandexSansText-Light.ttf) format('truetype'); +} + +@font-face { + font-family: Yandex Sans; + src: url(./fonts/YandexSansText-Regular.woff) format('woff'), + url(./fonts/YandexSansText-Regular.ttf) format('truetype'); +} + +@font-face { + font-family: Yandex Sans; + font-weight: 500; + src: url(./fonts/YandexSansText-Medium.woff) format('woff'), + url(./fonts/YandexSansText-Medium.ttf) format('truetype'); +} + +@font-face { + font-family: Helvetica Neue; + font-weight: bold; + src: url(./fonts/HelveticaNeue-Bold.woff) format('woff'), + url(./fonts/HelveticaNeue-Bold.ttf) format('truetype'); +} + +@font-face { + font-family: Helvetica Neue; + font-weight: 500; + src: url(./fonts/HelveticaNeue-Medium.woff) format('woff'), + url(./fonts/HelveticaNeue-Medium.ttf) format('truetype'); +} + +@font-face { + font-family: Helvetica Neue; + font-weight: normal; + src: url(./fonts/HelveticaNeue.woff) format('woff'), + url(./fonts/HelveticaNeue.ttf) format('truetype'); +} + body { padding: 0; - margin: 0; + margin: 0 calc(100% - 100vw) 0 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; + + overflow-x: hidden; } code { diff --git a/src/index.jsx b/src/index.jsx index ffc72ee..3dad6ca 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,8 +1,14 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { BrowserRouter } from 'react-router-dom'; -import { App } from './app'; +import { App } from './components/app'; import './index.css'; -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/src/react-utils/index.js b/src/react-utils/index.js new file mode 100644 index 0000000..54b2926 --- /dev/null +++ b/src/react-utils/index.js @@ -0,0 +1 @@ +export * from './react-utils'; diff --git a/src/react-utils/react-utils.jsx b/src/react-utils/react-utils.jsx new file mode 100644 index 0000000..f785624 --- /dev/null +++ b/src/react-utils/react-utils.jsx @@ -0,0 +1,20 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; + +const parseWithDate = json => + JSON.parse(json, (key, value) => (key === 'date' ? new Date(value) : value)); + +export const useSessionStorage = (key, defaultValue) => { + const [state, setState] = useState(parseWithDate(sessionStorage.getItem(key)) || defaultValue); + useEffect(() => sessionStorage.setItem(key, JSON.stringify(state)), [state]); + return [state, setState]; +}; + +const callbackWithDependencies = func => callback => ({ + deps(...deps) { + return func(callback, [...deps]); + } +}); + +export const cb = callbackWithDependencies(useCallback); + +export const mem = callbackWithDependencies(useMemo); diff --git a/src/utils/countdown-latch.js b/src/utils/countdown-latch.js new file mode 100644 index 0000000..c755303 --- /dev/null +++ b/src/utils/countdown-latch.js @@ -0,0 +1,11 @@ +export function createLatch(count, callback) { + return { + count, + countDown() { + this.count--; + if (this.count === 0) { + callback(); + } + } + }; +} diff --git a/src/utils/generators/index.js b/src/utils/generators/index.js new file mode 100644 index 0000000..cf348fb --- /dev/null +++ b/src/utils/generators/index.js @@ -0,0 +1 @@ +export { generateLetter } from './letter-generator'; diff --git a/src/utils/generators/letter-generator.js b/src/utils/generators/letter-generator.js new file mode 100644 index 0000000..d9b62fb --- /dev/null +++ b/src/utils/generators/letter-generator.js @@ -0,0 +1,14 @@ +import generateText from './text-generator'; +import generateUser from './user-generator'; +import random from './random'; + +export function generateLetter() { + return { + user: generateUser(), + content: { + subject: generateText(1, 1), + body: generateText(random.int(4, 5), random.int(5, 10)) + }, + date: new Date() + }; +} diff --git a/src/utils/generators/markov-chain.js b/src/utils/generators/markov-chain.js new file mode 100644 index 0000000..614230d --- /dev/null +++ b/src/utils/generators/markov-chain.js @@ -0,0 +1,63 @@ +function generateChain(text) { + const sentences = text.split(/(\.+)/).filter(s => s.length > 0 && s !== '.'); + + const graph = []; + + for (const sentence of sentences.values()) { + const addSymbol = (from, to) => { + if (graph[from] == null) graph[from] = []; + + if (graph[from][to] == null) graph[from][to] = 1; + else graph[from][to]++; + }; + + const symbols = sentence.split(/\s+/).filter(s => s.length > 0); + if (symbols.length > 0) { + addSymbol('markov-start', symbols[0]); + for (let cur = 0, next = 1; next < symbols.length; cur++, next++) { + addSymbol(symbols[cur], symbols[next]); + } + addSymbol(symbols.slice(-1), 'markov-end'); + } + } + + for (const from of Object.keys(graph)) { + let sum = 0; + for (const to of Object.keys(graph[from])) sum += graph[from][to]; + for (const to of Object.keys(graph[from])) graph[from][to] /= sum; + } + + return graph; +} + +export default function createMarkovChain(sampleText) { + const chain = generateChain(sampleText); + + return { + sequence() { + const choose = probs => { + const prob = Math.random(); + + let sum = 0; + for (const to of Object.keys(probs)) { + if (prob < sum + probs[to]) return to; + sum += probs[to]; + } + return 'markov-end'; + }; + + const seq = ['markov-start']; + + let curSymbol = seq[0]; + while (curSymbol !== 'markov-end') { + curSymbol = choose(chain[curSymbol]); + seq.push(curSymbol); + } + + seq.shift(); + seq.pop(); + + return seq; + } + }; +} diff --git a/src/utils/generators/random.js b/src/utils/generators/random.js new file mode 100644 index 0000000..ada9266 --- /dev/null +++ b/src/utils/generators/random.js @@ -0,0 +1,15 @@ +const random = { + int(from, to) { + return Math.floor(Math.random() * (to - from)) + from; + }, + + boolean() { + return this.int(0, 2) === 0; + }, + + element(array) { + return array.length === 0 ? undefined : array[this.int(0, array.length)]; + } +}; + +export default random; diff --git a/src/utils/generators/text-generator.js b/src/utils/generators/text-generator.js new file mode 100644 index 0000000..69f9872 --- /dev/null +++ b/src/utils/generators/text-generator.js @@ -0,0 +1,26 @@ +import createMarkovChain from './markov-chain'; + +const sampleText = `Согласно ежегодному исследованию StackOverflow, самая популярная профессия среди пользователей сервиса в 2017 году — это Web developer. Именно в эту категорию входят все фронтенд-разработчики. +Если зайти на первый попавшийся сайт по поиску работы, например, на hh.ru, создастся впечатление, что фронтенд-разработчик — это специалист-хамелеон. +Начинается все с путаницы в названиях вакансий: можно встретить и «front-end developer», и «front end разработчик», и «фронтендщик», и «фронтенд девелопер», и «web developer», и «фронтенд-разработчик». Иногда даже можно увидеть какого-нибудь «веб-верстальщика» с требованиями под фулстак-разработчика. Реакция на это одна: WTF?! +Беда в том, что часть работодателей не отличают (или не хотят отличать) верстальщика от фронтенд-разработчика, — это понятно по описанию вакансий. Разберемся, какие умения отделяют фронтенд-разработчика от «верстака» (верстальщики, не обижайтесь, вы тоже хорошие). +Верстальщик — боец узкого фронта. Его задача — сверстать полученный от дизайнера макет, используя HTML+CSS. Он, возможно, немного умеет в JavaScript, но чаще ограничивается умением прикрутить какой-нибудь плагин jQuery. +Фронтенд-разработчик не просто верстает макеты. Он хорошо знает JavaScript, разбирается во фреймворках и библиотеках (и активно юзает часть из них), понимает, что находится «под капотом» на серверной стороне. Его не пугают препроцессоры и сборщики LESS, SASS, GRUNT, GULP, он умеет работать с DOM, API, SVG-объектами, AJAX и CORS, может составлять SQL-запросы и копаться в данных. Получается сборная солянка навыков, к которым добавляется понимание принципов UI/UX-проектирования, адаптивной и отзывчивой верстки, кросс-браузерности и кросс-платформенности, а иногда и навыков мобильной разработки. +Фронтендщик в обязательном порядке умеет работать с контролем версий (Git, GitHub, CVS и т. д.), использовать графические редакторы, «играть» с шаблонами различных CMS. +Еще крайне желательно знать английский язык, чтобы не переводить спецификацию в Гугл-переводчике, уметь работать в команде, иногда мультиязычной, разбираться в веб-шрифтах, ну и понимать тестировщиков и сам процесс тестирования. `; + +const markovChain = createMarkovChain(sampleText); + +export default function generateText(paragraphsInText, sentencesInParagraph) { + const paragraphs = []; + for (let p = 0; p < paragraphsInText; p++) { + const sentences = []; + for (let s = 0; s < sentencesInParagraph; s++) { + const sentence = markovChain.sequence().join(' '); + sentences[s] = `${sentence[0].toUpperCase() + sentence.substr(1)}.`; + } + paragraphs[p] = sentences.join(' '); + } + + return paragraphs.join(' '); +} diff --git a/src/utils/generators/user-generator.js b/src/utils/generators/user-generator.js new file mode 100644 index 0000000..4cc92b6 --- /dev/null +++ b/src/utils/generators/user-generator.js @@ -0,0 +1,28 @@ +import random from './random'; + +const names = { + firstNames: ['Богдан', 'Виктор', 'Елисей', 'Ринат', 'Сидор', 'Фёдор'], + lastNames: ['Иванов', 'Смирнов', 'Кузнецов', 'Васильев', 'Петров', 'Михайлов', 'Федоров'] +}; + +function randomName() { + const firstName = random.element(names.firstNames); + const lastName = random.element(names.lastNames); + + return `${firstName} ${lastName}`; +} + +function randomImgUrl() { + return `https://randomuser.me/api/portraits/men/${random.int(0, 100)}.jpg`; +} + +function imageOrNull() { + return random.boolean() ? randomImgUrl() : null; +} + +export default function generateUser() { + return { + name: randomName(), + imageUrl: imageOrNull() + }; +} diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..9d91073 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,3 @@ +export * from './generators'; +export * from './utils'; +export * from './countdown-latch'; diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 0000000..5d89a31 --- /dev/null +++ b/src/utils/utils.js @@ -0,0 +1,33 @@ +export const initials = name => { + return name + .split(/\s+/) + .map(s => s.charAt(0)) + .join('') + .substring(0, 2) + .toUpperCase(); +}; + +export const randomColor = () => { + const hsvToRGB = (h, s, v) => { + const h1 = Math.floor(h * 6); + const f = h * 6 - h1; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][h1]; + return rgb.map(c => Math.floor(c * 256)); + }; + + return hsvToRGB(Math.random(), 0.4, 0.85); +}; + +export const formatDatetime = date => { + return `${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()}`; +}; + +export const toRussianDate = date => { + // noinspection JSUnresolvedFunction + return new Intl.DateTimeFormat('ru', { day: 'numeric', month: 'short' }).format(date); +}; + +export const mergeObjects = (obj1, obj2) => Object.assign({}, obj1, obj2);