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 (
-
- );
- }
-}
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);