diff --git a/package-lock.json b/package-lock.json
index 937a670..9cdd9ef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1233,6 +1233,19 @@
"@types/node": "*"
}
},
+ "@types/jest": {
+ "version": "24.0.13",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.13.tgz",
+ "integrity": "sha512-3m6RPnO35r7Dg+uMLj1+xfZaOgIHHHut61djNjzwExXN4/Pm9has9C6I1KMYSfz7mahDhWUOVg4HW/nZdv5Pww==",
+ "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 +1259,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": "12.0.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.4.tgz",
+ "integrity": "sha512-j8YL2C0fXq7IONwl/Ud5Kt0PeXw22zGERt+HSSnwbKOJVsAGkEz3sFCYwaF9IOuoG1HOtE0vKCj6sXF7Q0+Vaw=="
+ },
+ "@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 +1274,23 @@
"integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==",
"dev": true
},
+ "@types/react": {
+ "version": "16.8.19",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.19.tgz",
+ "integrity": "sha512-QzEzjrd1zFzY9cDlbIiFvdr+YUmefuuRYrPxmkwG0UQv5XF35gFIi7a95m1bNVcFU0VimxSZ5QVGSiBmlggQXQ==",
+ "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/tapable": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.2.tgz",
@@ -4890,6 +4924,11 @@
"cssom": "0.3.x"
}
},
+ "csstype": {
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.5.tgz",
+ "integrity": "sha512-JsTaiksRsel5n7XwqPAfB0l3TFKdpjW/kgAELf9vrb5adGA7UCPLajKK5s3nFrcFm3Rkyp/Qkgl73ENc1UY3cA=="
+ },
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
diff --git a/package.json b/package.json
index 9ede333..9d58f27 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,10 @@
"not op_mini all"
],
"dependencies": {
+ "@types/jest": "24.0.13",
+ "@types/node": "12.0.4",
+ "@types/react": "16.8.19",
+ "@types/react-dom": "16.8.4",
"react": "16.8.6",
"react-dom": "16.8.6"
},
@@ -27,12 +31,12 @@
"test": "react-scripts test",
"build": "react-scripts build",
"lint": "npm-run-all lint:*",
- "lint:js": "eslint '**/*.{js,jsx}'",
- "lint:css": "stylelint '**/*.css'",
+ "lint:js": "eslint \"**/*.{js,jsx}\"",
+ "lint:css": "stylelint \"**/*.css\"",
"lint-fix": "npm-run-all lint-fix:*",
- "lint-fix:js": "eslint --fix '**/*.{js,jsx}'",
- "lint-fix:css": "stylelint --fix '**/*.css'",
- "format": "prettier --write '**/*.{js,jsx,css,json,md}'",
+ "lint-fix:js": "eslint --fix \"**/*.{js,jsx}\"",
+ "lint-fix:css": "stylelint --fix \"**/*.css\"",
+ "format": "prettier --write \"**/*.{js,jsx,css,json,md}\"",
"now-build": "npm run build"
}
}
diff --git a/public/fonts/HelveticaNeue.ttf b/public/fonts/HelveticaNeue.ttf
new file mode 100644
index 0000000..7864fce
Binary files /dev/null and b/public/fonts/HelveticaNeue.ttf differ
diff --git a/public/fonts/Yandex Sans Display-Regular.ttf b/public/fonts/Yandex Sans Display-Regular.ttf
new file mode 100644
index 0000000..29a8017
Binary files /dev/null and b/public/fonts/Yandex Sans Display-Regular.ttf differ
diff --git a/public/images/cat-face.png b/public/images/cat-face.png
new file mode 100644
index 0000000..b86d178
Binary files /dev/null and b/public/images/cat-face.png differ
diff --git a/public/images/logo.png b/public/images/logo.png
new file mode 100644
index 0000000..ae13ce7
Binary files /dev/null and b/public/images/logo.png differ
diff --git a/public/images/owl-face.jpg b/public/images/owl-face.jpg
new file mode 100644
index 0000000..829e062
Binary files /dev/null and b/public/images/owl-face.jpg differ
diff --git a/public/images/sova.png b/public/images/sova.png
new file mode 100644
index 0000000..e8b5dd7
Binary files /dev/null and b/public/images/sova.png differ
diff --git a/public/images/spam.png b/public/images/spam.png
new file mode 100644
index 0000000..6ab6df2
Binary files /dev/null and b/public/images/spam.png differ
diff --git a/public/index.html b/public/index.html
index 9a8ef8f..5a426cf 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
+ Почта
You need to enable JavaScript to run this 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.module.css b/src/app/app.module.css
new file mode 100644
index 0000000..f20cbff
--- /dev/null
+++ b/src/app/app.module.css
@@ -0,0 +1,21 @@
+@font-face {
+ font-family: Yandex Sans;
+ src: url('/fonts/Yandex Sans Display-Regular.ttf');
+}
+
+@font-face {
+ font-family: Helvetica Neue;
+ src: url('/fonts/HelveticaNeue.ttf');
+}
+
+html {
+ height: 100%;
+}
+
+body {
+ min-width: 800px;
+ height: 100%;
+ min-height: 500px;
+ margin: 0;
+ background-color: #e5eaf0;
+}
diff --git a/src/app/app.tsx b/src/app/app.tsx
new file mode 100644
index 0000000..0b68b68
--- /dev/null
+++ b/src/app/app.tsx
@@ -0,0 +1,16 @@
+import React, { Component } from 'react';
+import Header from './header/header';
+import Main from './main/main';
+
+import './app.module.css';
+
+export class App extends Component {
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
diff --git a/src/app/header/hamburger/hamburger.module.css b/src/app/header/hamburger/hamburger.module.css
new file mode 100644
index 0000000..30c407f
--- /dev/null
+++ b/src/app/header/hamburger/hamburger.module.css
@@ -0,0 +1,11 @@
+.hamburger {
+ width: 20px;
+ margin: 15px 11px 0px 0px;
+ float: left;
+}
+
+.hamburger__div {
+ height: 3px;
+ margin: 4px 0;
+ background-color: black;
+}
diff --git a/src/app/header/hamburger/hamburger.tsx b/src/app/header/hamburger/hamburger.tsx
new file mode 100644
index 0000000..31bfb4f
--- /dev/null
+++ b/src/app/header/hamburger/hamburger.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import styles from './hamburger.module.css';
+import headerStyles from './../header.module.css';
+
+function Header() {
+ const classes = `${styles.hamburger} ${headerStyles['header__inline-element']}`;
+ return (
+
+ );
+}
+
+export default Header;
diff --git a/src/app/header/header.module.css b/src/app/header/header.module.css
new file mode 100644
index 0000000..56161d2
--- /dev/null
+++ b/src/app/header/header.module.css
@@ -0,0 +1,14 @@
+.header {
+ width: 100%;
+ height: 56px;
+ box-sizing: border-box;
+ padding: 0px 20px 0px 22px;
+ font-family: Yandex Sans, sans-serif;
+ font-size: 15px;
+ text-align: center;
+}
+
+.header__inline-element {
+ display: inline-block;
+ text-align: left;
+}
diff --git a/src/app/header/header.tsx b/src/app/header/header.tsx
new file mode 100644
index 0000000..406393a
--- /dev/null
+++ b/src/app/header/header.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import Hamburger from './hamburger/hamburger';
+import Logo from './logo/logo';
+import Search from './search/search';
+
+import styles from './header.module.css';
+
+function Header() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default Header;
diff --git a/src/app/header/logo/logo.module.css b/src/app/header/logo/logo.module.css
new file mode 100644
index 0000000..9cff198
--- /dev/null
+++ b/src/app/header/logo/logo.module.css
@@ -0,0 +1,4 @@
+.logo {
+ margin-top: 12px;
+ float: left;
+}
diff --git a/src/app/header/logo/logo.tsx b/src/app/header/logo/logo.tsx
new file mode 100644
index 0000000..1a31d45
--- /dev/null
+++ b/src/app/header/logo/logo.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+
+import styles from './logo.module.css';
+import headerStyles from './../header.module.css';
+
+function Logo() {
+ const classes = `${styles.logo} ${headerStyles['header__inline-element']}`;
+ return (
+
+
+
+ );
+}
+
+export default Logo;
diff --git a/src/app/header/search/search.module.css b/src/app/header/search/search.module.css
new file mode 100644
index 0000000..24cad11
--- /dev/null
+++ b/src/app/header/search/search.module.css
@@ -0,0 +1,16 @@
+.header__search {
+ margin-top: 11px;
+}
+
+.search {
+ width: 301px;
+ height: 32px;
+ padding: 0 9px 0 9px;
+
+ border: none;
+ background-color: white;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 32px;
+}
diff --git a/src/app/header/search/search.tsx b/src/app/header/search/search.tsx
new file mode 100644
index 0000000..4eaa580
--- /dev/null
+++ b/src/app/header/search/search.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import styles from './search.module.css';
+import headerStyles from './../header.module.css';
+
+function Search() {
+ const classes = `${styles.search} ${styles.header__search} ${headerStyles['header__inline-element']}`;
+ return (
+
+ {/* Поиск
+ ×
*/}
+
+ );
+}
+
+export default Search;
diff --git a/src/app/main/mailbox/data.ts b/src/app/main/mailbox/data.ts
new file mode 100644
index 0000000..a1ac73f
--- /dev/null
+++ b/src/app/main/mailbox/data.ts
@@ -0,0 +1,205 @@
+export const MAX_VISIBLE_MAILS = 30;
+export const MIN_AUTO_MAIL_INTERVAL = 10;
+export const MIN_AUTO_MAIL_CONSEQUENT_SPAN_INTERVAL = 300000;
+export const MAX_AUTO_MAIL_INTERVAL = 600000;
+
+let mailCnt = 0;
+
+export class Mail {
+ id: number;
+
+ img: string;
+ author: string;
+ title: string;
+ date: string;
+ text: string;
+ state: string;
+
+ old: boolean;
+ checked: boolean;
+ deleted: boolean;
+
+ constructor({img, author, title, date, text, old, state = 'hidden'}: {img: string, author: string, title: string, date: string, text: string, old: boolean, state?: string}) {
+ this.img = img;
+ this.author = author;
+ this.title = title;
+ this.date = date;
+ this.text = text;
+ this.old = old;
+ this.state = state;
+ this.id = mailCnt++;
+ this.checked = false;
+ this.deleted = false;
+ }
+
+ setCheck(value: boolean) {
+ this.checked = value;
+ }
+
+ markDeleted() {
+ this.deleted = true;
+ }
+}
+
+export const PRELOADED_MAILS = [
+ new Mail({
+ img: 'images/owl-face.jpg',
+ author: 'Сова',
+ title: 'От совы',
+ date: 'Mar 9',
+ text: `Как падают кошки
+
+
+ Большинство представителей семейства кошачьих имеет склонность к обзору местности с высоты. Крупные лесные кошки-рыси вообше значительную часть времени проводят на деревьях, находясь в засаде или погоне за добычей. А львы и леопарды в саваннах Африки приспособились в жаркое время отдыхать на деревьях, распластавшись на ветках и опустив вниз лапы. Случается, однако, что кошки не удерживаются на высоте и падают. Но и в падении у них есть свои особенности. Многим приходилось наблюдать, как падает обыкновенная кошка, сорвавшись с карниза дома, с дерева или с забора. Сначала она падает к земле головой, спиной или боком, но затем, сделав резкий поворот в воздухе, вывертывается и становится на лапки. И так всегда. Как бы ни падала кошка, приземляется она всегда на лапки и тотчас же может бежать дальше. Такое мгновенное выравнивание положения тела у кошек обеспечивается действием ее вестибулярного аппарата.
+
+
+
+ При падении кошки вестибулярный аппарат помогает ей осуществить ряд последовательно возникающих рефлексов и приземлиться на лапы. Ненормальное положение тела в пространстве приводит в раздражение отолитовый прибор каналов внутреннего уха кошки. В ответ на это раздражение происходит рефлекторное сокращение мускулов шеи, приводящих голову животного в нормальное положение по отношению к горизонту. Это - первый рефлекс. Сокращение же шейных мышц и постановка шеи при повороте головы являются возбудителем для осуществления другого рефлекса - сокращения определенных мышц туловища и конечностей. В итоге животное принимает правильное положение.
+
+
+ Этот сложный врожденный цепной рефлекс выработался у некоторых животных как приспособление к образу жизни. Ведь животным, особенно из семейства кошачьих, часто приходится во время охоты прыгать и падать с деревьев, скал или со спины своей жертвы. И не будь у них этого приспособительного рефлекса, от них не только ушла бы добыча, но иной раз и самому охотнику пришлось бы пострадать от зубов, рогов или копыт своей жертвы.
+
+ Текст статьи взят с сайта petsi.net
+ `,
+ old: false,
+ state: 'showed'
+ }),
+ new Mail({
+ img: 'images/cat-face.png',
+ author: 'Кот',
+ title: 'Старое сообщение',
+ date: 'Feb 23',
+ text: 'Какое-то старое сообщение',
+ old: true,
+ state: 'showed'
+ }),
+ new Mail({
+ img: 'images/spam.png',
+ author: 'Неспамнеспамнеспамнеспам',
+ title: 'Легкий способ зарабатывать 10000000000 в секунду, нужно всего-лишь...',
+ date: 'Jan 1',
+ text: '[Читать продолжение в источнике]',
+ old: false,
+ state: 'showed'
+ }),
+];
+
+export const CAT_NAMES = [
+ 'Барсик',
+ 'Боня',
+ 'Бакс',
+ 'Алекс',
+ 'Бади',
+ 'Амур',
+ 'Абуссель',
+ 'Баксик',
+ 'Кузя',
+ 'Персик',
+ 'Абрек',
+ 'Абрикос',
+ 'Тимоша',
+ 'Авалон',
+ 'Саймон',
+ 'Бурбузяка Жабс',
+ 'Марсик',
+ 'Абу',
+ 'Маркиз',
+ 'Аадон',
+ 'Дымок',
+ 'Лаки',
+ 'Сёма',
+ 'Симба',
+ 'Абрамович',
+ 'Пушок',
+ 'Айс',
+ 'Бося',
+ 'Кекс',
+ 'Басик',
+ 'Алмаз',
+ 'Макс',
+ 'Гарфилд',
+ 'Феликс',
+ 'Том',
+ 'Тиша',
+ 'Тишка',
+ 'Цезарь',
+ 'Мася',
+ 'Абакан',
+ 'Лакки',
+ 'Васька',
+ 'Марсель',
+ 'Адольф',
+ 'Вася',
+ 'Бабасик',
+ 'Зевс',
+ 'Вольт',
+ 'Лео',
+ 'Адидас',
+ 'Зефир',
+ 'Максик',
+ 'Вайс',
+ 'Барс',
+ 'Кокос',
+ 'Рыжик',
+ 'Мартин',
+ 'Айс-Крим',
+ 'Томас',
+ 'Филя',
+ 'Нафаня',
+ 'Дарсик',
+ 'Марс',
+ 'Валера',
+ 'Абориген',
+ 'Тошка',
+ 'Базиль',
+ 'Сосисыч',
+ 'Абрико',
+ 'Масик',
+ 'Абус',
+ 'Абсент',
+ 'Умка',
+ 'Жужа',
+ 'Веня',
+ 'Каспер',
+ 'Грей',
+ 'Живчик',
+ 'Убийца мышей',
+ 'Глюк',
+ 'Патрик',
+ 'Оптимус Прайм',
+ 'Виски',
+ 'Акакий',
+ 'Симка',
+ 'Тёма',
+ 'Баффи',
+ 'Аватар',
+ 'Гаврик',
+ 'Жан батист Гренуй',
+ 'Ганс',
+ 'Вегас',
+ 'Гаврюша',
+ 'Авдон',
+ 'Вин Дизель',
+ 'Вафлик',
+ 'Бонни',
+ 'Снежок',
+ 'Люцифер',
+ 'Базилио',
+ 'Тима',
+ 'Байрон'
+];
+
+export const MONTHS = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec'
+];
diff --git a/src/app/main/mailbox/footer/footer.module.css b/src/app/main/mailbox/footer/footer.module.css
new file mode 100644
index 0000000..1de51e4
--- /dev/null
+++ b/src/app/main/mailbox/footer/footer.module.css
@@ -0,0 +1,22 @@
+.footer {
+ position: absolute;
+ bottom: 0;
+
+ width: 100%;
+ height: 35px;
+
+ border-top: 1px solid #e2e2e2;
+ color: #9b9b9b;
+ font-size: 11px;
+ line-height: 35px;
+}
+
+.footer__inline-element {
+ margin-right: 20px;
+ float: right;
+}
+
+.footer__link {
+ color: #9b9b9b;
+ text-decoration: none;
+}
diff --git a/src/app/main/mailbox/footer/footer.tsx b/src/app/main/mailbox/footer/footer.tsx
new file mode 100644
index 0000000..869fc51
--- /dev/null
+++ b/src/app/main/mailbox/footer/footer.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import styles from './footer.module.css';
+
+function Footer() {
+ return (
+
+
© 2019 – 2019, Ямдекс
+
+
+
+ );
+}
+
+export default Footer;
diff --git a/src/app/main/mailbox/helper.ts b/src/app/main/mailbox/helper.ts
new file mode 100644
index 0000000..86a8b9e
--- /dev/null
+++ b/src/app/main/mailbox/helper.ts
@@ -0,0 +1,66 @@
+import { Mail, CAT_NAMES, MONTHS } from './data';
+
+function getRandomInt(min: number, max: number) {
+ return Math.floor(Math.random() * (max - min)) + min;
+}
+
+function getAuthor() {
+ // Cat names
+ const names = CAT_NAMES;
+ return names[getRandomInt(0, names.length)];
+}
+
+function getCurrentTime() {
+ return new Date().getTime();
+}
+
+function getDate() {
+ const date = new Date();
+ const months = MONTHS;
+ const day = date.getDate();
+ const monthIndex = date.getMonth();
+ return `${months[monthIndex]} ${day}`;
+}
+
+function getImg() {
+ const size = 100 + (getCurrentTime() % 200);
+ return `http://placekitten.com/${size}/${size}`;
+}
+
+function sendRequest(link: string) {
+ const xhttp = new XMLHttpRequest();
+ let res = '';
+ xhttp.onreadystatechange = () => {
+ if (xhttp.readyState === 4 && xhttp.status === 200) {
+ res = xhttp.responseText;
+ }
+ };
+ xhttp.open('GET', link, false);
+ xhttp.send();
+ return res;
+}
+
+function getTitle() {
+ return sendRequest(
+ 'https://baconipsum.com/api/?type=all-meat&sentences=1&start-with-lorem=0&format=text'
+ );
+}
+
+function getText() {
+ return sendRequest(
+ 'https://baconipsum.com/api/?type=all-meat¶graphs=5&start-with-lorem=1&format=text'
+ );
+}
+
+function generateMail() {
+ return new Mail({
+ img: getImg(),
+ author: getAuthor(),
+ title: getTitle(),
+ date: getDate(),
+ text: getText(),
+ old: false,
+ });
+}
+
+export { generateMail, getCurrentTime, getRandomInt };
diff --git a/src/app/main/mailbox/mail/mail.module.css b/src/app/main/mailbox/mail/mail.module.css
new file mode 100644
index 0000000..19a5a3e
--- /dev/null
+++ b/src/app/main/mailbox/mail/mail.module.css
@@ -0,0 +1,128 @@
+.mailbox__mail {
+ width: 100%;
+ height: 40px;
+
+ border-bottom: 1px solid #e2e2e2;
+ color: #cccccc;
+ font-size: 13px;
+ line-height: 40px;
+ overflow-y: hidden;
+}
+
+.mailbox__mail-element {
+ display: inline-block;
+ overflow: hidden;
+ vertical-align: middle;
+}
+
+.checkbox {
+ display: inline-block;
+ margin: 0px 10px 0px 20px;
+ vertical-align: middle;
+}
+
+.checkbox__input {
+ position: relative;
+
+ display: none;
+ float: left;
+}
+
+.checkbox__span {
+ width: 16px;
+ height: 16px;
+
+ border: 1px solid;
+ border-radius: 3px;
+ float: left;
+}
+
+.checkbox__input:checked + .checkbox__span:before {
+ content: '✓';
+ float: left;
+ font-size: 16px;
+ font-weight: bold;
+ line-height: 16px;
+}
+
+.mail__pic {
+ margin-right: 10px;
+}
+
+.pic__img {
+ width: 30px;
+ height: 30px;
+ vertical-align: middle;
+}
+
+.mail__author {
+ width: 155px;
+ color: black;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.mail__title {
+ width: calc(100% - 355px);
+ box-sizing: border-box;
+ padding-left: 10px;
+ color: black;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.mail__new {
+ font-weight: 700;
+}
+
+.mail__dot {
+ width: 10px;
+ height: 10px;
+ background-color: #6287bd;
+ border-radius: 50%;
+ visibility: hidden;
+}
+
+.mail__new .mail__dot {
+ visibility: visible;
+}
+
+.mail__time {
+ width: 50px;
+ margin-right: 20px;
+ color: #969799;
+ float: right;
+ font-weight: 500;
+ text-align: right;
+}
+
+.mailbox__mail[data-state='collapsed'] {
+ height: 0px;
+
+ border: none;
+ transition: height 0.4s;
+}
+
+.mailbox__mail[data-state='init'] {
+ height: 0px;
+
+ border-width: 0px;
+
+ animation: appearance 0.4s 1 ease-out forwards;
+}
+
+@keyframes appearance {
+ from {
+ height: 0px;
+ background-color: #6287bd;
+ }
+ to {
+ height: 40px;
+ background-color: white;
+ }
+}
+
+.mailbox__mail[data-state='showed'] {
+ height: 40px;
+ transition: height 0.4s;
+}
diff --git a/src/app/main/mailbox/mail/mail.tsx b/src/app/main/mailbox/mail/mail.tsx
new file mode 100644
index 0000000..dd7322a
--- /dev/null
+++ b/src/app/main/mailbox/mail/mail.tsx
@@ -0,0 +1,70 @@
+import React, { Component } from 'react';
+
+import { Mail as MailType } from '../data';
+import { MailBoxState } from '../mailbox'
+
+import styles from './mail.module.css';
+
+interface MailProps {
+ mail: MailType;
+ onClick: () => void;
+ onAnimationEnd: () => void;
+}
+
+export class Mail extends Component {
+ trigger: React.RefObject;
+
+ constructor(props: Readonly) {
+ super(props);
+ this.trigger = React.createRef();
+ }
+
+ render() {
+ const { mail, onClick, onAnimationEnd } = this.props;
+ const checkboxId = `checkbox_${mail.id}`;
+
+ return (
+ {
+ if (event.key === 'Enter') {
+ const current = this.trigger.current;
+ if (current) {
+ current.click();
+ }
+ }
+ }}
+ >
+
+
+ mail.setCheck(event.target.checked)}
+ />
+
+
+
+
+
+
{mail.author}
+
+
{mail.title}
+
{mail.date}
+
+
+ );
+ }
+}
+
+export default Mail;
diff --git a/src/app/main/mailbox/mailbox.module.css b/src/app/main/mailbox/mailbox.module.css
new file mode 100644
index 0000000..e668890
--- /dev/null
+++ b/src/app/main/mailbox/mailbox.module.css
@@ -0,0 +1,119 @@
+button {
+ display: inherit;
+ padding: inherit;
+
+ border: inherit;
+ background-color: inherit;
+ color: inherit;
+ cursor: pointer;
+ font: inherit;
+ font-family: inherit;
+ outline: inherit;
+ text-align: inherit;
+}
+
+.mailbox {
+ position: relative;
+
+ overflow: hidden;
+ height: 100%;
+ box-sizing: border-box;
+ background-color: white;
+ border-radius: 3px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.34);
+ font-family: Helvetica Neue, sans-serif;
+}
+
+.checkbox {
+ display: inline-block;
+ margin: 0px 10px 0px 20px;
+ vertical-align: middle;
+}
+
+.checkbox__input {
+ position: relative;
+
+ display: none;
+ float: left;
+}
+
+.checkbox__span {
+ width: 16px;
+ height: 16px;
+
+ border: 1px solid;
+ border-radius: 3px;
+ float: left;
+}
+
+.checkbox__input:checked + .checkbox__span:before {
+ content: '✓';
+ float: left;
+ font-size: 16px;
+ font-weight: bold;
+ line-height: 16px;
+}
+
+.mailbox__header {
+ top: 0;
+
+ width: 100%;
+ height: 35px;
+
+ border-bottom: 1px solid #e2e2e2;
+ color: #cccccc;
+ font-size: 13px;
+ font-weight: 500;
+ line-height: 35px;
+}
+
+.mailbox__header-element {
+ display: inline-block;
+ margin: 0px 14px 0px 10px;
+}
+
+.mailbox__mail-contents {
+ position: absolute;
+ top: 36px;
+ bottom: 35px;
+
+ display: none;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 20px;
+ margin: 0 auto;
+ background-color: white;
+ overflow-y: scroll;
+ overflow-y: auto;
+}
+
+.mailbox__msg-close {
+ margin: 0px 10px;
+ color: grey;
+ float: right;
+ font-size: 40px;
+ font-weight: bold;
+}
+
+.mailbox__mail-list {
+ position: absolute;
+ top: 36px;
+ bottom: 35px;
+
+ width: 100%;
+ box-sizing: border-box;
+ margin: 0 auto;
+ overflow-y: scroll;
+}
+
+.mailbox__trigger {
+ display: none;
+}
+
+.mailbox__trigger:checked ~ .mailbox__mail-contents {
+ display: block;
+}
+
+.mailbox__trigger:checked + .mailbox__mail-list {
+ display: none;
+}
diff --git a/src/app/main/mailbox/mailbox.tsx b/src/app/main/mailbox/mailbox.tsx
new file mode 100644
index 0000000..f803079
--- /dev/null
+++ b/src/app/main/mailbox/mailbox.tsx
@@ -0,0 +1,216 @@
+import React, { Component } from 'react';
+import Footer from './footer/footer';
+import MailItem from './mail/mail';
+
+import styles from './mailbox.module.css';
+
+import { generateMail, getCurrentTime, getRandomInt } from './helper';
+import {
+ Mail,
+ PRELOADED_MAILS,
+ MAX_VISIBLE_MAILS,
+ MIN_AUTO_MAIL_INTERVAL,
+ MIN_AUTO_MAIL_CONSEQUENT_SPAN_INTERVAL,
+ MAX_AUTO_MAIL_INTERVAL
+} from './data';
+
+export interface MailBoxState {
+ currentMail: number;
+ mails: Array;
+}
+
+export class MailBox extends Component<{}, MailBoxState> {
+ // local non-concurrent data
+ private previousMailTime: number = 0;
+ private penultimateMailTime: number = 0;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+
+ this.updateState = this.updateState.bind(this);
+ this.newMail = this.newMail.bind(this);
+ this.autoMails = this.autoMails.bind(this);
+ this.deleteSelected = this.deleteSelected.bind(this);
+ this.allCheck = this.allCheck.bind(this);
+
+ this.state = {
+ currentMail: 0,
+ mails: PRELOADED_MAILS
+ };
+
+ // initialize automatic generation of mails
+ const timeout = getRandomInt(MIN_AUTO_MAIL_INTERVAL, MAX_AUTO_MAIL_INTERVAL);
+ setTimeout(this.autoMails, timeout);
+ }
+
+ componentDidMount() {
+ (window as any).newMail = this.newMail;
+ }
+
+ updateState(genState: (state: MailBoxState) => MailBoxState) {
+ this.setState((prevState) => genState(prevState));
+ }
+
+ updateVisibility(mails: Array) {
+ let visibleMails = MAX_VISIBLE_MAILS;
+ return mails.map(mail => {
+ if (mail.deleted) return mail;
+
+ const newMail = mail;
+
+ if (visibleMails > 0) {
+ visibleMails--;
+ if (mail.state === 'collapsed') {
+ newMail.state = 'showed';
+ }
+ } else if (mail.state === 'showed') {
+ newMail.state = 'collapsed';
+ }
+
+ return newMail;
+ });
+ };
+
+
+ autoMails() {
+ const currentTime = getCurrentTime();
+ const timeout = getRandomInt(
+ Math.max(
+ MIN_AUTO_MAIL_INTERVAL,
+ Math.min(currentTime - this.penultimateMailTime, MIN_AUTO_MAIL_CONSEQUENT_SPAN_INTERVAL)
+ ),
+ MAX_AUTO_MAIL_INTERVAL
+ );
+ this.penultimateMailTime = this.previousMailTime;
+ this.previousMailTime = currentTime + timeout;
+ this.newMail();
+ setTimeout(this.autoMails, timeout);
+ }
+
+ newMail() {
+ const newMail = generateMail();
+ newMail.state = 'init';
+
+ this.updateState(prevState => {
+ const { mails, currentMail } = prevState;
+ const newMails = mails;
+ newMails.unshift(newMail);
+ return {
+ currentMail,
+ mails: this.updateVisibility(newMails)
+ };
+ });
+ }
+
+ deleteSelected() {
+ this.updateState(prevState => {
+ const { mails, currentMail } = prevState;
+ const newMails = mails.map(mail => {
+ const newMail = mail;
+ if (mail.checked) {
+ newMail.state = 'collapsed';
+ newMail.markDeleted();
+ }
+ return newMail;
+ });
+ return {
+ currentMail,
+ mails: this.updateVisibility(newMails)
+ };
+ });
+ }
+
+ allCheck(value: boolean) {
+ this.updateState(prevState => {
+ const { mails, currentMail } = prevState;
+ const newMails = mails.map(mail => {
+ const newMail = mail;
+ newMail.setCheck(value);
+ return newMail;
+ });
+ return {
+ currentMail,
+ mails: this.updateVisibility(newMails)
+ };
+ });
+ }
+
+ render() {
+ const currentMail = this.state.mails.find(mail => mail.id === this.state.currentMail);
+ const mailHTML = currentMail ? currentMail.text : "";
+
+ return (
+
+
+
+ this.allCheck(event.target.checked)}/>
+
+
+
+ Переслать
+
+
+ Удалить
+
+
+ Это спам
+
+
+ Прочитано
+
+
+
+
+
+ {this.state.mails.map((mail, index) => (
+
+ this.updateState(prevState => {
+ return {
+ currentMail: mail.id,
+ mails: prevState.mails
+ };
+ })
+ }
+ onAnimationEnd={() => {
+ this.updateState(prevState => {
+ const { currentMail, mails } = prevState;
+ let newMails = mails;
+ if (mail.state === 'init') {
+ newMails[index].state = 'showed';
+ }
+ if (mail.deleted) {
+ newMails = mails.filter(curMail => curMail.id !== mail.id);
+ }
+ return {
+ currentMail,
+ mails: newMails
+ };
+ });
+ }}
+ />
+ ))}
+
+
+
+
+
+ );
+ }
+}
+
+export default MailBox;
diff --git a/src/app/main/main.module.css b/src/app/main/main.module.css
new file mode 100644
index 0000000..297e474
--- /dev/null
+++ b/src/app/main/main.module.css
@@ -0,0 +1,12 @@
+.main {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+
+ min-width: inherit;
+ min-height: inherit;
+ box-sizing: border-box;
+ margin: 56px 20px 14px 22px;
+}
diff --git a/src/app/main/main.tsx b/src/app/main/main.tsx
new file mode 100644
index 0000000..5010511
--- /dev/null
+++ b/src/app/main/main.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import MailWrapper from './mailbox/mailbox';
+import SidePanel from './side-panel/side-panel';
+
+import styles from './main.module.css';
+
+function Main() {
+ return (
+
+
+
+
+ );
+}
+
+export default Main;
diff --git a/src/app/main/side-panel/side-panel.module.css b/src/app/main/side-panel/side-panel.module.css
new file mode 100644
index 0000000..42616dd
--- /dev/null
+++ b/src/app/main/side-panel/side-panel.module.css
@@ -0,0 +1,51 @@
+.side-panel {
+ margin-right: 22px;
+ float: left;
+ font-family: Helvetica Neue, sans-serif;
+}
+
+button {
+ display: inherit;
+ padding: inherit;
+
+ border: inherit;
+ background-color: inherit;
+ color: inherit;
+ cursor: pointer;
+ font: inherit;
+ font-family: inherit;
+ outline: inherit;
+ text-align: inherit;
+}
+
+.side-panel__button {
+ width: 147px;
+ box-sizing: border-box;
+ padding: 0 10px;
+ border-radius: 3px;
+}
+
+.side-panel__button_primary {
+ height: 32px;
+ margin-bottom: 8px;
+ background-color: #6287bd;
+ color: white;
+ font-size: 12px;
+ line-height: 32px;
+ text-align: center;
+}
+
+.side-panel__button_secondary {
+ height: 22px;
+ margin: 1px 0;
+ color: #707070;
+ font-size: 11px;
+ font-weight: 500;
+ line-height: 22px;
+}
+
+.side-panel__button_active {
+ background-color: #cdd6e4;
+ color: #555555;
+ font-weight: 700;
+}
diff --git a/src/app/main/side-panel/side-panel.tsx b/src/app/main/side-panel/side-panel.tsx
new file mode 100644
index 0000000..8b23ca8
--- /dev/null
+++ b/src/app/main/side-panel/side-panel.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+
+import styles from './side-panel.module.css';
+
+function SidePanel() {
+ return (
+
+
+ Написать
+
+
+ Входящие
+
+
+ Отправленные
+
+
+ Удалённые
+
+
+ Спам
+
+
+ Черновики
+
+
+ Создать папку
+
+
+ );
+}
+
+export default SidePanel;
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index 2b6e525..0000000
--- a/src/index.css
+++ /dev/null
@@ -1,12 +0,0 @@
-body {
- 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;
-}
diff --git a/src/index.jsx b/src/index.jsx
index ffc72ee..ce996f0 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -3,6 +3,4 @@ import ReactDOM from 'react-dom';
import { App } from './app';
-import './index.css';
-
ReactDOM.render( , document.getElementById('root'));
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/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"
+ ]
+}