diff --git a/.stylelintrc b/.stylelintrc index 8d41721..0df797a 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,3 +1,8 @@ { - "extends": ["@hellroot/stylelint-config"] + "extends": ["@hellroot/stylelint-config"], + "rules": { + "selector-pseudo-class-no-unknown": [true, { + "ignorePseudoClasses": ["global"] + }] + } } diff --git a/.travis.yml b/.travis.yml index 93c4d63..8e325a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,4 @@ language: node_js node_js: - "10" script: - - npm run test - npm run lint diff --git a/package-lock.json b/package-lock.json index 937a670..ca2f398 100644 --- a/package-lock.json +++ b/package-lock.json @@ -949,7 +949,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" }, @@ -957,8 +956,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==" } } }, @@ -1233,6 +1231,19 @@ "@types/node": "*" } }, + "@types/jest": { + "version": "24.0.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.12.tgz", + "integrity": "sha512-60sjqMhat7i7XntZckcSGV8iREJyXXI6yFHZkSZvCPUeOnEJ/VP1rU/WpEWQ56mvoh8NhC+sfKAuJRTyGtCOow==", + "requires": { + "@types/jest-diff": "*" + } + }, + "@types/jest-diff": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jest-diff/-/jest-diff-20.0.1.tgz", + "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==" + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1246,10 +1257,14 @@ "dev": true }, "@types/node": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.5.tgz", - "integrity": "sha512-/OMMBnjVtDuwX1tg2pkYVSqRIDSmNTnvVvmvP/2xiMAAWf4a5+JozrApCrO4WCAILmXVxfNoQ3E+0HJbNpFVGg==", - "dev": true + "version": "11.13.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.8.tgz", + "integrity": "sha512-szA3x/3miL90ZJxUCzx9haNbK5/zmPieGraZEe4WI+3srN0eGLiT22NXeMHmyhNEopn+IrxqMc7wdVwvPl8meg==" + }, + "@types/prop-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz", + "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==" }, "@types/q": { "version": "1.5.2", @@ -1257,6 +1272,33 @@ "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", "dev": true }, + "@types/react": { + "version": "16.8.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.15.tgz", + "integrity": "sha512-dMhzw1rWK+wwJWvPp5Pk12ksSrm/z/C/+lOQbMZ7YfDQYnJ02bc0wtg4EJD9qrFhuxFrf/ywNgwTboucobJqQg==", + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "@types/react-dom": { + "version": "16.8.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.4.tgz", + "integrity": "sha512-eIRpEW73DCzPIMaNBDP5pPIpK1KXyZwNgfxiVagb5iGiz6da+9A5hslSX6GAQKdO7SayVCS/Fr2kjqprgAvkfA==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-virtualized": { + "version": "9.21.1", + "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.1.tgz", + "integrity": "sha512-BwWXk6Vy+YuWbc2jZsmm0fn8bglPUpqUWPH/JUUBfvfKfL2nDvvmCiauyxMCWrxZMVBbkxaUuP82SviaDv0wGw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, "@types/tapable": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.2.tgz", @@ -2652,7 +2694,6 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -2661,8 +2702,7 @@ "core-js": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==", - "dev": true + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" } } }, @@ -4085,6 +4125,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", @@ -4890,6 +4935,11 @@ "cssom": "0.3.x" } }, + "csstype": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.4.tgz", + "integrity": "sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==" + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -5317,6 +5367,14 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "dom-serializer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", @@ -13557,6 +13615,11 @@ "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-scripts": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-2.1.8.tgz", @@ -13705,6 +13768,19 @@ } } }, + "react-virtualized": { + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.0.tgz", + "integrity": "sha512-duKD2HvO33mqld4EtQKm9H9H0p+xce1c++2D5xn59Ma7P8VT7CprfAe5hwjd1OGkyhqzOZiTMlTal7LxjH5yBQ==", + "requires": { + "babel-runtime": "^6.26.0", + "classnames": "^2.2.3", + "dom-helpers": "^2.4.0 || ^3.0.0", + "loose-envify": "^1.3.0", + "prop-types": "^15.6.0", + "react-lifecycles-compat": "^3.0.4" + } + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -14109,8 +14185,7 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regenerator-transform": { "version": "0.13.4", diff --git a/package.json b/package.json index 9ede333..dac6f16 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,18 @@ "not op_mini all" ], "dependencies": { + "@types/jest": "24.0.12", + "@types/node": "11.13.8", + "@types/react": "16.8.15", + "@types/react-dom": "16.8.4", "react": "16.8.6", - "react-dom": "16.8.6" + "react-dom": "16.8.6", + "react-virtualized": "9.21.0" }, "devDependencies": { "@hellroot/eslint-config": "1.8.0", "@hellroot/stylelint-config": "1.1.0", + "@types/react-virtualized": "9.21.1", "eslint": "5.12.0", "npm-run-all": "4.1.5", "prettier": "1.17.0", diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index a11777c..0000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/index.html b/public/index.html index 9a8ef8f..4f4d4b1 100644 --- a/public/index.html +++ b/public/index.html @@ -2,16 +2,15 @@ - + - React App + Яндекс.Почта -
diff --git a/public/ya-mail-favicon.png b/public/ya-mail-favicon.png new file mode 100644 index 0000000..555ec0b Binary files /dev/null and b/public/ya-mail-favicon.png differ 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.module.css b/src/app/app.module.css new file mode 100644 index 0000000..789c862 --- /dev/null +++ b/src/app/app.module.css @@ -0,0 +1,8 @@ +.app { + width: available; + min-width: 950px; + min-height: 100%; + margin-right: 22px; + margin-left: 22px; + font-family: 'Helvetica Neue', sans-serif; +} 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/app/app.tsx b/src/app/app.tsx new file mode 100644 index 0000000..58d7061 --- /dev/null +++ b/src/app/app.tsx @@ -0,0 +1,157 @@ +import React, { Component } from 'react'; + +import styles from './app.module.css'; + +import Header from '../header'; +import MainBlock from '../main-block'; + +export interface MessageInterface { + id: number, + senderName: String, + senderLogo: String, + subject: String, + date: String, + hiddenText: String, + selected: boolean, + shrink: boolean, + unshrink: boolean +} + +interface State { + messagesList: MessageInterface[], + messageIsOpen: boolean +} + +export class App extends Component { + private messagesPerPage: number; + private timeoutUpper: number; + private timeoutLower: number; + + private senders: String[]; + private subjects: String[]; + private texts: String[]; + private months: String[]; + + public state: State; + + constructor(props: Readonly<{}>) { + super(props); + this.newMail = this.newMail.bind(this); + this.createAndRandom = this.createAndRandom.bind(this); + this.newRandomMessage = this.newRandomMessage.bind(this); + this.buildNewMessage = this.buildNewMessage.bind(this); + this.updateList = this.updateList.bind(this); + + this.messagesPerPage = 1000; + + this.senders = ['Петя', 'Вася', 'Маша']; + this.subjects = ['Привет из России', 'Hello from England', 'Bonjour de France']; + this.texts = ['Привет!', 'Hello!', 'Bonjour!']; + this.months = [ + 'январь', + 'февраль', + 'март', + 'апрель', + 'май', + 'июнь', + 'июль', + 'август', + 'сентябрь', + 'октябрь', + 'ноябрь', + 'декабрь' + ]; + this.timeoutUpper = 10 * 60 * 1000; + this.timeoutLower = 5 * 60 * 1000; + + this.state = { + messagesList: [], + messageIsOpen: false + }; + } + + componentDidMount() { + const thisHolder = this; + thisHolder.createAndRandom(); + } + + createAndRandom() { + this.newMail(); + this.newRandomMessage(); + } + + newRandomMessage() { + setTimeout( + this.createAndRandom, + Math.random() * (this.timeoutUpper - this.timeoutLower) + this.timeoutLower + ); + } + + newMail() { + this.setState((prevState: State) => { + const newMessagesList = prevState.messagesList; + const newMessage = this.buildNewMessage(); + + newMessagesList.unshift(newMessage); + + setTimeout(() => { + newMessage.unshrink = true; + this.setState({ + messagesList: newMessagesList + }); + }, 50); + + return { + messagesList: newMessagesList + }; + }); + } + + buildNewMessage(): MessageInterface { + const currentDate = new Date(); + + const id = currentDate.getTime(); + const langInd = Math.floor(Math.random() * this.senders.length); + const hiddenText = this.texts[langInd]; + + const monthInd = currentDate.getMonth(); + const month = this.months[monthInd]; + const day = currentDate.getDate(); + + const senderName = this.senders[Math.floor(Math.random() * this.senders.length)]; + return { + id, + senderName, + senderLogo: senderName[0], + subject: this.subjects[langInd], + date: `${day} ${month.substr(0, 3)}`, + hiddenText, + selected: false, + shrink: false, + unshrink: false + }; + } + + updateList(newList: MessageInterface[]) { + this.setState(() => { + return { + messagesList: newList + } + }) + } + + render() { + return ( +
+
+ +
+ ); + } +} + +export default App; diff --git a/src/app/index.js b/src/app/index.ts similarity index 100% rename from src/app/index.js rename to src/app/index.ts diff --git a/src/header/header.module.css b/src/header/header.module.css new file mode 100644 index 0000000..74380dd --- /dev/null +++ b/src/header/header.module.css @@ -0,0 +1,3 @@ +.mail-page-header { + margin-top: 11px; +} diff --git a/src/header/header.tsx b/src/header/header.tsx new file mode 100644 index 0000000..963fdf5 --- /dev/null +++ b/src/header/header.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import styles from './header.module.css'; + +import Menu from './menu/menu'; +import YandexServiceLogo from './yandex-service-logo/yandex-service-logo'; +import SearchBar from './search-bar/search-bar'; +import MessageCreator from './message-creator/message-creator'; + +interface InjectedProps { + newMailFunction: () => void; +} + +class Header extends React.Component { + render() { + return ( +
+ + + + +
+ ); + } +} + +export default Header; diff --git a/src/header/index.ts b/src/header/index.ts new file mode 100644 index 0000000..0d3f295 --- /dev/null +++ b/src/header/index.ts @@ -0,0 +1,2 @@ +import Header from './header' +export default Header diff --git a/src/header/menu/menu.module.css b/src/header/menu/menu.module.css new file mode 100644 index 0000000..b777c25 --- /dev/null +++ b/src/header/menu/menu.module.css @@ -0,0 +1,20 @@ +.header__menu { + display: inline-block; + width: 20px; + height: 17px; + margin-top: -5px; + margin-right: 10px; + cursor: pointer; + outline: none; + vertical-align: middle; +} + +.header__menu:focus { + outline: 2px solid #d9f7ff; +} + +.menu__stripe { + height: 2px; + margin-top: 4px; + background-color: #000; +} diff --git a/src/header/menu/menu.tsx b/src/header/menu/menu.tsx new file mode 100644 index 0000000..e9698cc --- /dev/null +++ b/src/header/menu/menu.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import styles from './menu.module.css'; + +function Menu() { + return ( + { + click.preventDefault(); + }}> +
+
+
+ + ); +} + +export default Menu; diff --git a/src/header/message-creator/message-creator.module.css b/src/header/message-creator/message-creator.module.css new file mode 100644 index 0000000..1c62b55 --- /dev/null +++ b/src/header/message-creator/message-creator.module.css @@ -0,0 +1,5 @@ +.header__message-creator { + cursor: pointer; + float: right; + opacity: 0.5; +} diff --git a/src/header/message-creator/message-creator.tsx b/src/header/message-creator/message-creator.tsx new file mode 100644 index 0000000..4eba7b7 --- /dev/null +++ b/src/header/message-creator/message-creator.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import styles from './message-creator.module.css'; + +interface InjectedProps { + newMailFunction: () => void; +} + +class MessageCreator extends React.Component { + render() { + return ( + + ); + } +} + +export default MessageCreator; diff --git a/src/header/search-bar/search-bar.module.css b/src/header/search-bar/search-bar.module.css new file mode 100644 index 0000000..8973fb1 --- /dev/null +++ b/src/header/search-bar/search-bar.module.css @@ -0,0 +1,25 @@ +.header__search-bar { + display: inline-block; + width: 35%; + height: 34px; + padding-right: 10px; + padding-left: 16px; + margin-left: 12.5%; + + border-width: 0; + background: rgba(242, 244, 247, 1); + box-shadow: inset 0 0 0 1px rgba(217, 219, 222, 1); + color: #000; + + font: 15px 'Helvetica Neue', sans-serif; + line-height: 2; + outline: none; +} + +.header__search-bar:focus { + outline: 2px solid #d9f7ff; +} + +.header__search-bar::placeholder { + color: #727477; +} diff --git a/src/header/search-bar/search-bar.tsx b/src/header/search-bar/search-bar.tsx new file mode 100644 index 0000000..5132690 --- /dev/null +++ b/src/header/search-bar/search-bar.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import styles from './search-bar.module.css'; + +class SearchBar extends React.Component { + render() { + return ; + } +} + +export default SearchBar; diff --git a/src/header/yandex-service-logo/yandex-service-logo.module.css b/src/header/yandex-service-logo/yandex-service-logo.module.css new file mode 100644 index 0000000..a031172 --- /dev/null +++ b/src/header/yandex-service-logo/yandex-service-logo.module.css @@ -0,0 +1,21 @@ +.header__yandex-service-logo { + display: inline-block; + width: 200px; + height: 31px; + vertical-align: middle; +} + +.yandex-service-logo__image-link { + display: inline-block; + cursor: pointer; + outline: none; +} + +.yandex-service-logo__image-link:focus { + outline: 2px solid #d9f7ff; +} + +.yandex-service-logo__image { + width: 153px; + height: 31px; +} diff --git a/src/header/yandex-service-logo/yandex-service-logo.tsx b/src/header/yandex-service-logo/yandex-service-logo.tsx new file mode 100644 index 0000000..094372c --- /dev/null +++ b/src/header/yandex-service-logo/yandex-service-logo.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import styles from './yandex-service-logo.module.css'; + +import YandexMail from '../../resources/yandex-mail.png'; + +class YandexServiceLogo extends React.Component { + render() { + return ( +
+ + Логотип Яндекс почты + +
+ ); + } +} + +export default YandexServiceLogo; diff --git a/src/index.css b/src/index.css index 2b6e525..355b134 100644 --- a/src/index.css +++ b/src/index.css @@ -1,12 +1,7 @@ body { + width: 100%; + height: 100%; padding: 0; margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', - 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; + background: #e5eaf0; } diff --git a/src/index.jsx b/src/index.jsx index ffc72ee..f6073ef 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,7 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { App } from './app'; +import { App } from './app/app'; import './index.css'; diff --git a/src/main-block/index.ts b/src/main-block/index.ts new file mode 100644 index 0000000..f80c28e --- /dev/null +++ b/src/main-block/index.ts @@ -0,0 +1,2 @@ +import MainBlock from './main-block' +export default MainBlock diff --git a/src/main-block/left-menu/folders-list/folders-list.module.css b/src/main-block/left-menu/folders-list/folders-list.module.css new file mode 100644 index 0000000..dc5c20a --- /dev/null +++ b/src/main-block/left-menu/folders-list/folders-list.module.css @@ -0,0 +1,30 @@ +.folders-list { + margin-top: 12px; +} + +.selected { + background-color: #cdd6e4; + border-radius: 4px; + color: #555; + font-weight: 700; +} + +.folders-list__folder { + overflow: hidden; + height: 14px; + padding: 5px 10px; + color: #707070; + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.folders-list__folder:hover { + background-color: #d8e1ef; + border-radius: 4px; + cursor: pointer; +} + +.selected:hover { + background-color: #cdd6e4; +} diff --git a/src/main-block/left-menu/folders-list/folders-list.tsx b/src/main-block/left-menu/folders-list/folders-list.tsx new file mode 100644 index 0000000..cc15e94 --- /dev/null +++ b/src/main-block/left-menu/folders-list/folders-list.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import styles from './folders-list.module.css'; + +class FoldersList extends React.Component{ + render() { + return ( +
+
Входящие
+
Отправленные
+
Удаленные
+
Спам
+
Черновики
+
Создать папку
+
+ ); + } +} + +export default FoldersList; diff --git a/src/main-block/left-menu/index.ts b/src/main-block/left-menu/index.ts new file mode 100644 index 0000000..ee2e47f --- /dev/null +++ b/src/main-block/left-menu/index.ts @@ -0,0 +1,2 @@ +import LeftMenu from './left-menu' +export default LeftMenu diff --git a/src/main-block/left-menu/left-menu.module.css b/src/main-block/left-menu/left-menu.module.css new file mode 100644 index 0000000..d74d4e5 --- /dev/null +++ b/src/main-block/left-menu/left-menu.module.css @@ -0,0 +1,24 @@ +.main-block__left-menu { + display: inline-block; + width: 19.5%; + min-width: 150px; +} + +.left-menu__write-letter { + overflow: hidden; + height: 32px; + padding: 0 10px; + background-color: #6287bd; + border-radius: 4px; + color: #fff; + font-size: 12px; + line-height: 32px; + + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.left-menu__write-letter:hover { + cursor: pointer; +} diff --git a/src/main-block/left-menu/left-menu.tsx b/src/main-block/left-menu/left-menu.tsx new file mode 100644 index 0000000..6a291bd --- /dev/null +++ b/src/main-block/left-menu/left-menu.tsx @@ -0,0 +1,17 @@ +import React, { ReactComponentElement } from 'react'; + +import styles from './left-menu.module.css'; +import FoldersList from './folders-list/folders-list'; + +class LeftMenu extends React.Component { + render() { + return ( +
+
Написать
+ +
+ ); + } +} + +export default LeftMenu; diff --git a/src/main-block/main-block.module.css b/src/main-block/main-block.module.css new file mode 100644 index 0000000..7437726 --- /dev/null +++ b/src/main-block/main-block.module.css @@ -0,0 +1,5 @@ +.mail-page__main-block { + min-width: 800px; + min-height: 100%; + padding-top: 13px; +} diff --git a/src/main-block/main-block.tsx b/src/main-block/main-block.tsx new file mode 100644 index 0000000..bcde187 --- /dev/null +++ b/src/main-block/main-block.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import styles from './main-block.module.css'; +import LeftMenu from './left-menu'; +import MessagesBlock from './messages-block'; +import { MessageInterface } from '../app'; + +interface InjectedProps { + messagesList: MessageInterface[]; + messagesPerPage: number; + updateList: (newList: MessageInterface[]) => void; +} + +class MainBlock extends React.Component { + render() { + return ( +
+ + +
+ ); + } +} + +export default MainBlock; diff --git a/src/main-block/messages-block/footer/footer.module.css b/src/main-block/messages-block/footer/footer.module.css new file mode 100644 index 0000000..5a40335 --- /dev/null +++ b/src/main-block/messages-block/footer/footer.module.css @@ -0,0 +1,35 @@ +.footer { + width: 100%; + height: 34px; + + border-top: solid 1px #e2e2e2; +} + +.footer__links-list { + display: inline; + padding: 5px 20px; + margin: 5px 0; + float: right; + font-family: 'Helvetica Neue', sans-serif; + font-size: 11px; +} + +.link-container { + display: inline; +} + +.footer-link { + display: inline-block; + overflow: hidden; + max-width: 200px; + margin-left: 20px; + color: #ccc; + cursor: pointer; + outline: none; + text-overflow: ellipsis; + white-space: nowrap; +} + +.footer-link:focus { + outline: 2px solid #d9f7ff; +} diff --git a/src/main-block/messages-block/footer/footer.tsx b/src/main-block/messages-block/footer/footer.tsx new file mode 100644 index 0000000..945619e --- /dev/null +++ b/src/main-block/messages-block/footer/footer.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import styles from './footer.module.css'; + +class Footer extends React.Component{ + render() { + return ( + + ); + } +} + +export default Footer; diff --git a/src/main-block/messages-block/footer/index.ts b/src/main-block/messages-block/footer/index.ts new file mode 100644 index 0000000..cd82817 --- /dev/null +++ b/src/main-block/messages-block/footer/index.ts @@ -0,0 +1,2 @@ +import Footer from './footer' +export default Footer diff --git a/src/main-block/messages-block/header/header.module.css b/src/main-block/messages-block/header/header.module.css new file mode 100644 index 0000000..eb52409 --- /dev/null +++ b/src/main-block/messages-block/header/header.module.css @@ -0,0 +1,53 @@ +.messages-block-header { + overflow: auto; + height: 40px; + + border-bottom: solid 1px #e2e2e2; + font-family: 'Helvetica Neue', sans-serif; + font-size: 13px; + white-space: nowrap; +} + +.messages-block-header__action { + display: inline-block; + width: auto; + + margin-left: 15px; + color: #ccc; + cursor: pointer; + font-weight: 500; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; +} + +.button-to-div { + padding: 0; + + border: 0; + font-family: inherit; + font-size: 100%; + outline: none; +} + +.button-to-div:focus { + outline: 2px solid #d9f7ff; +} + +#check-all:global { + margin-top: 12px; + margin-right: 10px; + margin-left: 20px; +} + +#check-all-label:global { + display: inline-block; + width: 46px; + height: 40px; + cursor: pointer; + vertical-align: middle; +} + +.cursor-pointer { + cursor: pointer; +} diff --git a/src/main-block/messages-block/header/header.tsx b/src/main-block/messages-block/header/header.tsx new file mode 100644 index 0000000..a711e17 --- /dev/null +++ b/src/main-block/messages-block/header/header.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import styles from './header.module.css'; +import checkboxStyle from '../messages-block.module.css' + +interface InjectedProps { + handleSelectAll: () => void; + deleteSelected: () => void; + selectAllCheckbox: boolean; +} + +class Header extends React.Component { + render() { + return ( +
+ + + + + +
+ ); + } +} + +export default Header; diff --git a/src/main-block/messages-block/header/index.ts b/src/main-block/messages-block/header/index.ts new file mode 100644 index 0000000..0d3f295 --- /dev/null +++ b/src/main-block/messages-block/header/index.ts @@ -0,0 +1,2 @@ +import Header from './header' +export default Header diff --git a/src/main-block/messages-block/hidden-message/hidden-message.module.css b/src/main-block/messages-block/hidden-message/hidden-message.module.css new file mode 100644 index 0000000..da7e406 --- /dev/null +++ b/src/main-block/messages-block/hidden-message/hidden-message.module.css @@ -0,0 +1,38 @@ +.hidden-message { + position: relative; + z-index: 1; + top: 0; + + display: none; + min-height: 500px; + + border-bottom: solid 1px #e2e2e2; + background-color: #fff; +} + +.hidden-message__open { + display: block; +} + +.hidden-message__closed { + display: none; +} + +.hidden-message__content { + overflow: auto; + padding: 20px; + font-family: 'Helvetica Neue', sans-serif; + font-size: 14px; + white-space: normal; +} + +.close-message { + margin-right: 5px; + + border: 0; + color: #ccc; + cursor: pointer; + float: right; + font-size: 35px; + font-weight: 100; +} diff --git a/src/main-block/messages-block/hidden-message/hidden-message.tsx b/src/main-block/messages-block/hidden-message/hidden-message.tsx new file mode 100644 index 0000000..4ce74c6 --- /dev/null +++ b/src/main-block/messages-block/hidden-message/hidden-message.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import styles from './hidden-message.module.css'; + +interface InjectedProps { + closeMessage: () => void; + messageIsOpen: boolean; + hiddenMessageText: String; +} + +class HiddenMessage extends React.Component { + render() { + const classAddition = this.props.messageIsOpen ? '__open' : '__closed'; + return ( +
+ + +
+ {this.props.hiddenMessageText} +
+
+ ); + } +} + +export default HiddenMessage; diff --git a/src/main-block/messages-block/hidden-message/index.ts b/src/main-block/messages-block/hidden-message/index.ts new file mode 100644 index 0000000..9536208 --- /dev/null +++ b/src/main-block/messages-block/hidden-message/index.ts @@ -0,0 +1,2 @@ +import HiddenMessage from './hidden-message' +export default HiddenMessage diff --git a/src/main-block/messages-block/index.ts b/src/main-block/messages-block/index.ts new file mode 100644 index 0000000..54f0a44 --- /dev/null +++ b/src/main-block/messages-block/index.ts @@ -0,0 +1,2 @@ +import MessagesBlock from './messages-block' +export default MessagesBlock diff --git a/src/main-block/messages-block/message/index.ts b/src/main-block/messages-block/message/index.ts new file mode 100644 index 0000000..3a718b6 --- /dev/null +++ b/src/main-block/messages-block/message/index.ts @@ -0,0 +1,2 @@ +import Message from './message' +export default Message diff --git a/src/main-block/messages-block/message/message.module.css b/src/main-block/messages-block/message/message.module.css new file mode 100644 index 0000000..35758c9 --- /dev/null +++ b/src/main-block/messages-block/message/message.module.css @@ -0,0 +1,124 @@ +.select-message__checkbox { + margin-top: 12px; + margin-right: 10px; + margin-left: 20px; +} + +.select-message__checkbox-label { + display: inline-block; + width: 46px; + height: 40px; + cursor: pointer; + vertical-align: middle; +} + +.message-container { + width: calc(100% - 46px - 3px); + padding: 0; + padding-right: 3px; + + border: 0; + cursor: pointer; + font-family: inherit; + font-size: 100%; + outline: none; +} + +.message-container:focus { + box-shadow: 0 0 0 2px #d9f7ff; +} + +.message { + overflow: hidden; + height: 40px; + + border-bottom: solid 0 #e2e2e2; + font-family: 'Helvetica Neue', sans-serif; + font-size: 13px; + opacity: 0; + text-overflow: ellipsis; + transition: opacity 1.5s, height 1.5s, border-bottom-width 1.5s; + white-space: nowrap; + will-change: transform, height, opacity; +} + +.message-info__sender-logo { + display: inline-block; + width: 30px; + height: 30px; + background-color: #f33; + + border-radius: 100px; + color: #fff; + line-height: 30px; + text-align: center; + vertical-align: middle; +} + +.bold { + font-weight: bold; +} + +.message-info__sender { + display: inline-block; + overflow: hidden; + width: 175px; + margin-left: 10px; + color: #000; + line-height: 30px; + text-align: left; + text-overflow: ellipsis; + vertical-align: middle; + + white-space: nowrap; +} + +.message-info__mark { + display: inline-block; + width: 10px; + height: 10px; + margin-right: 10px; + margin-left: 10px; + vertical-align: middle; +} + +.unread-mark { + background-color: #6287bd; + border-radius: 100px; +} + +.message-info__subject { + display: inline-block; + overflow: hidden; + width: calc(100% - 325px); + color: #000; + text-align: left; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; +} + +.message-info__date-container { + display: inline-block; + overflow: hidden; + width: 70px; + margin-right: 10px; + margin-left: 0; + color: #9b9b9b; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; +} + +.date-container__date { + float: right; +} + +.unshrink { + border-bottom: solid 1px #e2e2e2; + opacity: 1; +} + +.shrink { + opacity: 0; +} diff --git a/src/main-block/messages-block/message/message.tsx b/src/main-block/messages-block/message/message.tsx new file mode 100644 index 0000000..f0c0ca8 --- /dev/null +++ b/src/main-block/messages-block/message/message.tsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import styles from './message.module.css'; +import checkboxStyle from '../messages-block.module.css' +import { MessageInterface } from '../../../app'; + +interface InjectedProps { + message: MessageInterface; + openMessage: (message: MessageInterface) => void; + selectCheckbox: (messageIndex: number) => void; + messageIndex: number; +} + +class Message extends React.Component { + render() { + const classAddition = + (this.props.message.unshrink ? 'unshrink' : '') + + (this.props.message.shrink ? 'shrink' : ''); + return ( +
+ + + +
+ ); + } +} + +export default Message; diff --git a/src/main-block/messages-block/messages-block.module.css b/src/main-block/messages-block/messages-block.module.css new file mode 100644 index 0000000..270c35d --- /dev/null +++ b/src/main-block/messages-block/messages-block.module.css @@ -0,0 +1,49 @@ +.messages-block { + display: inline-block; + width: 78%; + min-width: 600px; + height: 100%; + margin-right: 0; + margin-left: 2%; + background-color: #fff; + border-radius: 4px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + + vertical-align: top; +} + +.messages-list { + overflow: auto; + min-height: 500px; + max-height: 1231px; + + border-bottom: solid 1px #e2e2e2; +} + +.messages-list__open { + display: block; +} + +.messages-list__closed { + display: none; +} + +.checkbox { + display: inline-block; + width: 16px; + height: 16px; + + border: 1px solid rgba(0, 0, 0, 0.2); + background-color: #fff; + + border-radius: 3px; + cursor: pointer; +} + +.virtualizedList { + outline: none; +} + +.virtualizedList:focus { + outline: 2px solid #d9f7ff; +} diff --git a/src/main-block/messages-block/messages-block.tsx b/src/main-block/messages-block/messages-block.tsx new file mode 100644 index 0000000..f9a39a0 --- /dev/null +++ b/src/main-block/messages-block/messages-block.tsx @@ -0,0 +1,138 @@ +import React from 'react'; + +import styles from './messages-block.module.css'; +import Header from './header'; +import HiddenMessage from './hidden-message'; +import Message from './message'; +import Footer from './footer'; +import { MessageInterface } from '../../app'; +import { AutoSizer, List } from 'react-virtualized'; + +interface InjectedProps { + messagesList: MessageInterface[]; + messagesPerPage: number; + updateList: (newList: MessageInterface[]) => void; +} + +interface State { + hiddenMessageText: String, + messageIsOpen: boolean, + selectAllCheckbox: boolean, + messagesList: MessageInterface[] +} + +class MessagesBlock extends React.Component { + public state: State; + + constructor(props: InjectedProps) { + super(props); + this.openMessage = this.openMessage.bind(this); + this.closeMessage = this.closeMessage.bind(this); + + this.handleSelectAll = this.handleSelectAll.bind(this); + this.selectCheckbox = this.selectCheckbox.bind(this); + + this.deleteSelectedMessages = this.deleteSelectedMessages.bind(this); + + this.state = { + hiddenMessageText: '', + messageIsOpen: false, + selectAllCheckbox: false, + messagesList: this.props.messagesList + }; + } + + closeMessage() { + this.setState({ + messageIsOpen: false + }); + } + + openMessage(message: MessageInterface) { + this.setState({ + messageIsOpen: true, + hiddenMessageText: message.hiddenText + }); + } + + handleSelectAll() { + this.setState((prevState: State) => { + const newMessagesList = prevState.messagesList; + for (let i = 0; i < newMessagesList.length; i++) { + newMessagesList[i].selected = !prevState.selectAllCheckbox; + } + + return { + selectAllCheckbox: !prevState.selectAllCheckbox, + messagesList: newMessagesList + }; + }); + } + + selectCheckbox(messageIndex: number) { + this.setState((prevState: State) => { + const newMessagesList = prevState.messagesList; + newMessagesList[messageIndex].selected = !newMessagesList[messageIndex].selected; + return { + messagesList: newMessagesList + }; + }); + } + + deleteSelectedMessages() { + this.setState((prevState: State) => { + const newMessagesList = prevState.messagesList.filter(message => !message.selected); + this.props.updateList(newMessagesList); + }); + } + + render() { + this.state.messagesList = this.props.messagesList; + this.state.selectAllCheckbox = this.state.messagesList.some(message => message.selected); + + const messagesListClassAddition = !this.state.messageIsOpen ? '__open' : '__closed'; + const messagesCount = this.props.messagesList.length < this.props.messagesPerPage ? + this.props.messagesList.length : this.props.messagesPerPage; + return ( +
+
+ +
+ + {({ height, width }) => ( + { + return
+ +
; + } + } + /> + )} +
+
+
+
+ ); + } +} + +export default MessagesBlock; diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/resources/yandex-mail.png b/src/resources/yandex-mail.png new file mode 100644 index 0000000..08b572a Binary files /dev/null and b/src/resources/yandex-mail.png differ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0980b23 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve" + }, + "include": [ + "src" + ] +}