diff --git a/package.json b/package.json
index 9ede333..ed2b041 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,13 @@
"not op_mini all"
],
"dependencies": {
+ "@types/jest": "24.0.12",
+ "@types/node": "11.13.9",
+ "@types/react": "16.8.15",
+ "@types/react-dates": "17.1.5",
+ "@types/react-dom": "16.8.4",
"react": "16.8.6",
+ "react-dates": "20.1.0",
"react-dom": "16.8.6"
},
"devDependencies": {
diff --git a/public/index.html b/public/index.html
index 9a8ef8f..5d57a79 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,7 +8,7 @@
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.module.css b/src/app/app.module.css
new file mode 100644
index 0000000..741334c
--- /dev/null
+++ b/src/app/app.module.css
@@ -0,0 +1,12 @@
+:global(body) :local(.light) {
+ background-color: #e5eaf0;
+}
+
+:global(body) :local(.dark) {
+ background-color: #212121;
+}
+
+.app {
+ min-width: 900px;
+ min-height: 100%;
+}
diff --git a/src/app/app.tsx b/src/app/app.tsx
new file mode 100644
index 0000000..e6d91ac
--- /dev/null
+++ b/src/app/app.tsx
@@ -0,0 +1,272 @@
+import React, {Component} from 'react';
+
+import styles from './app.module.css';
+import {Header} from './components/header/Header';
+import {MainBlock} from './components/main-block/MainBlock';
+import {Menu} from './components/menu/Menu';
+import * as utils from './message-templates';
+import {ThemeProvider, themes} from "../theme/theme-context";
+import {Moment} from "moment";
+
+const maxMessageInterval = 10 * 60 * 1000;
+const timeMessageInterval = 5 * 60 * 1000;
+const maxMessagePerPage = 30;
+
+export interface IMessage {
+ id: string
+ theme: string
+ text: string
+ firstLetterSender: string
+ sender: string
+ date: Date
+ isChecked: boolean
+ toCreate: boolean
+ toDelete: boolean
+ display: boolean
+}
+
+interface IState {
+ wasNormalInterval: boolean
+ messages: IMessage[]
+ theme: themes
+ startDate: Moment | null,
+ endDate: Moment | null,
+ focusedInput: any
+}
+
+export class App extends Component<{}, IState> {
+ static createMessageValues(
+ id: string,
+ theme: string,
+ text: string,
+ firstLetterSender: string,
+ sender: string,
+ date: Date,
+ isChecked: boolean,
+ toCreate: boolean,
+ toDelete: boolean,
+ display: boolean
+ ) {
+ return {
+ id,
+ theme,
+ text,
+ firstLetterSender,
+ sender,
+ date,
+ isChecked,
+ toCreate,
+ toDelete,
+ display
+ };
+ }
+
+ static getGeneratedDate() {
+ return new Date();
+ }
+
+ static getRandomArbitrary(min: number, max: number) {
+ return Math.random() * (max - min) + min;
+ }
+
+ static async generateMessage() {
+ const id = new Date().getTime().toString();
+
+ const senderName = utils.getRandomSender();
+ const [theme, text] = await utils.getRandomThemeAndText();
+ const date = App.getGeneratedDate();
+ return App.createMessageValues(
+ id,
+ theme,
+ text,
+ senderName[0],
+ senderName,
+ date,
+ false,
+ false,
+ false,
+ true
+ );
+ }
+
+ constructor(props: {}) {
+ super(props);
+ this.state = {
+ messages: [],
+ wasNormalInterval: true,
+ theme: themes.light,
+ startDate: null,
+ endDate: null,
+ focusedInput: null
+ };
+ this.newMail = this.newMail.bind(this);
+ this.deleteMessages = this.deleteMessages.bind(this);
+ App.generateMessage = App.generateMessage.bind(this);
+ this.getTimeForMessage = this.getTimeForMessage.bind(this);
+ this.showHiddenMessages = this.showHiddenMessages.bind(this);
+ this.toggleTheme = this.toggleTheme.bind(this);
+ this.filterMessagesByDate = this.filterMessagesByDate.bind(this);
+ setTimeout(() => {this.addDefaultMessagesForTestingCalendar()}, 100);
+ setTimeout(() => {
+ this.newMail();
+ }, App.getRandomArbitrary(10, maxMessageInterval));
+ }
+
+ addDefaultMessagesForTestingCalendar() {
+ const newMessages = this.state.messages;
+ for (let i = 0; i < 10; i++) {
+ const curMessage = App.createMessageValues(i.toString(), `theme${i}`, `text${i}`, `S`,
+ `Sender${i}`, new Date(2019, 3, i + 25), false, false, false, true);
+ newMessages.unshift(curMessage);
+ setTimeout(() => {
+ curMessage.toCreate = true;
+ this.setState({
+ messages: newMessages
+ });
+ }, 10);
+ }
+ this.setState({messages: newMessages});
+ }
+
+ toggleTheme = () => {
+ this.setState((state: IState) => ({
+ theme:
+ state.theme === themes.dark
+ ? themes.light
+ : themes.dark,
+ }));
+ };
+
+ filterMessagesByDate(startDate: Date, endDate: Date) {
+ const messagesList = this.state.messages;
+ for (let i = 0; i < messagesList.length; i++) {
+ const curDate = messagesList[i].date.valueOf();
+ messagesList[i].display = curDate >= startDate.valueOf() && curDate <= endDate.valueOf();
+ }
+ this.setState({messages: messagesList})
+ }
+
+ getTimeForMessage() {
+ let randomTime = App.getRandomArbitrary(10, maxMessageInterval);
+ if (this.state.wasNormalInterval) {
+ if (randomTime < timeMessageInterval) {
+ this.setState({ wasNormalInterval: false });
+ }
+ } else {
+ randomTime = App.getRandomArbitrary(timeMessageInterval, maxMessageInterval);
+ this.setState({ wasNormalInterval: true });
+ }
+ return randomTime;
+ }
+
+ checkboxHandler = (id: string) => {
+ this.setState((prevState: IState) => {
+ const msgIndex = prevState.messages.findIndex(curMessage => curMessage.id.toString() === id);
+ const newMessages = prevState.messages;
+ newMessages[msgIndex].isChecked = !newMessages[msgIndex].isChecked;
+ return { messages: newMessages };
+ });
+ };
+
+ topBarCheckboxHandler = (isChecked: boolean) => {
+ this.setState((prevState: IState) => {
+ const newMessages = prevState.messages;
+ for (let i = 0; i < Math.min(prevState.messages.length, maxMessagePerPage); i++) {
+ newMessages[i] = prevState.messages[i];
+ newMessages[i].isChecked = isChecked;
+ }
+ return { messages: newMessages };
+ });
+ };
+
+ showHiddenMessages = (messagesList: IMessage[]) => {
+ let displayedNumber = 0;
+ let i = 0;
+ const showingMessagesList = messagesList;
+ while (displayedNumber < maxMessagePerPage && i < showingMessagesList.length) {
+ if (!showingMessagesList[i].toDelete) {
+ displayedNumber++;
+ showingMessagesList[i].display = true;
+ }
+ i++;
+ }
+ return showingMessagesList;
+ };
+
+ deleteMessages() {
+ const messagesList = this.state.messages;
+ for (let i = 0; i < messagesList.length; i++) {
+ if (messagesList[i].isChecked) {
+ messagesList[i].toDelete = true;
+ }
+ }
+ this.setState({ messages: this.showHiddenMessages(messagesList) });
+ setTimeout(() => {
+ this.setState({
+ messages: messagesList.filter(message => !message.isChecked)
+ });
+ }, 1000);
+ }
+
+ async newMail() {
+ const timeForMessage = this.getTimeForMessage();
+ const newMessage = await App.generateMessage();
+ this.setState((prevState: IState) => {
+ const newMessages = prevState.messages;
+ if (newMessages.length >= maxMessagePerPage) {
+ for (let i = maxMessagePerPage - 1; i < newMessages.length; i++) {
+ newMessages[i].display = false;
+ }
+ }
+ if (prevState.startDate != null && prevState.endDate != null) {
+ const curDate = newMessage.date.valueOf();
+ newMessage.display = curDate >= prevState.startDate.valueOf() && curDate <= prevState.endDate.valueOf();
+ }
+ newMessages.unshift(newMessage);
+ setTimeout(() => {
+ newMessage.toCreate = true;
+ this.setState({
+ messages: newMessages
+ });
+ }, 10);
+ return { messages: newMessages };
+ });
+ setTimeout(() => {
+ this.newMail();
+ }, timeForMessage);
+ }
+
+ handleDatesChange = ({startDate, endDate}: { startDate: Moment | null, endDate: Moment | null }): void => {
+ this.setState({startDate, endDate});
+ console.log(startDate + " " + endDate);
+ if (startDate === null || endDate === null) {
+ this.filterMessagesByDate(new Date(1970, 0, 1), new Date(2100, 0, 1));
+ } else {
+ this.filterMessagesByDate(startDate.toDate(), endDate.toDate());
+ }
+ };
+
+ updateFocus = (focusedInput: any) => {
+ this.setState({focusedInput: focusedInput});
+ };
+
+ render() {
+ const colorStyle = this.state.theme === themes.light ? styles.light : styles.dark;
+ return (
+
+
+
+
+ );
+ }
+}
diff --git a/src/app/components/header/Header.module.css b/src/app/components/header/Header.module.css
new file mode 100644
index 0000000..b10e88a
--- /dev/null
+++ b/src/app/components/header/Header.module.css
@@ -0,0 +1,10 @@
+.ya-logo {
+ display: inline-block;
+ height: 30px;
+ vertical-align: middle;
+}
+
+.header {
+ margin-right: 22px;
+ margin-left: 22px;
+}
diff --git a/src/app/components/header/Header.tsx b/src/app/components/header/Header.tsx
new file mode 100644
index 0000000..bcf65c7
--- /dev/null
+++ b/src/app/components/header/Header.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import styles from './Header.module.css';
+
+import yaLogoLight from '../../../resources/images/yandex-mail-light.png';
+import yaLogoDark from '../../../resources/images/yandex-mail-dark.png';
+import {Hamburger} from './hamburger/Hamburger';
+import {SearchBox} from './search-box/SearchBox';
+import ThemedButton from "./themed-button/ThemedButton";
+import {ThemeContext, themes} from "../../../theme/theme-context";
+import {DateRangePicker} from "react-dates";
+import {Moment} from "moment";
+import 'react-dates/initialize';
+import 'react-dates/lib/css/_datepicker.css';
+import './react_dates_overrides.css'
+
+interface IProps {
+ changeTheme: () => void
+ handleDatesChange: ( arg : {startDate: Moment | null, endDate: Moment | null}) => void
+ startDate: Moment | null,
+ endDate: Moment | null,
+ focusedInput: any
+ updateFocus: (focusedInput: any) => void
+}
+
+export class Header extends React.Component {
+ constructor(props: IProps) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
+

+
+
+ this.props.updateFocus(focusedInput)}
+ small
+ showClearDates
+ showDefaultInputIcon
+ isOutsideRange={() => false}
+ />
+
+
+
+ );
+ }
+}
+
+Header.contextType = ThemeContext;
diff --git a/src/app/components/header/hamburger/Hamburger.module.css b/src/app/components/header/hamburger/Hamburger.module.css
new file mode 100644
index 0000000..cb5ade2
--- /dev/null
+++ b/src/app/components/header/hamburger/Hamburger.module.css
@@ -0,0 +1,22 @@
+.single-strip {
+ height: 3px;
+ margin-top: 4px;
+}
+
+.hamburger {
+ display: inline-block;
+ width: 20px;
+ height: 17px;
+ margin-top: -5px;
+ margin-right: 5px;
+ cursor: pointer;
+ vertical-align: middle;
+}
+
+.dark {
+ background-color: white;
+}
+
+.light {
+ background-color: #000;
+}
diff --git a/src/app/components/header/hamburger/Hamburger.tsx b/src/app/components/header/hamburger/Hamburger.tsx
new file mode 100644
index 0000000..5f1e785
--- /dev/null
+++ b/src/app/components/header/hamburger/Hamburger.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+import styles from './Hamburger.module.css';
+import {ThemeContext, themes} from "../../../../theme/theme-context";
+
+export class Hamburger extends React.Component {
+ render() {
+ const colorStyle = this.context === themes.light ? styles.light : styles.dark;
+ return (
+
+ );
+ }
+}
+
+Hamburger.contextType = ThemeContext;
diff --git a/src/app/components/header/react_dates_overrides.css b/src/app/components/header/react_dates_overrides.css
new file mode 100644
index 0000000..4bee812
--- /dev/null
+++ b/src/app/components/header/react_dates_overrides.css
@@ -0,0 +1,51 @@
+.calendarWrapper {
+ display: inline-block;
+ margin-left: 350px;
+}
+
+.dark .DateRangePickerInput {
+ background-color: rgb(125, 125, 125);
+}
+
+.dark .DayPicker {
+ background-color: rgb(125, 125, 125);
+}
+
+.dark .DateInput_input {
+ background-color: rgb(125, 125, 125);
+ color: white;
+}
+
+.dark .DateInput_input::placeholder {
+ color: white;
+}
+
+.dark .CalendarDay__default {
+ background-color: rgb(207, 207, 207);
+}
+
+.dark .CalendarMonth {
+ background-color: rgb(125, 125, 125);
+}
+
+.dark .CalendarMonthGrid {
+ background-color: rgb(125, 125, 125);
+}
+
+.dark .DateRangePickerInput_calendarIcon_svg {
+ fill: white;
+}
+
+.dark .DayPicker_weekHeader {
+ color: white;
+}
+
+.dark .CalendarDay__selected_span {
+ background-color: #484848;
+}
+
+.dark .CalendarDay__selected,
+.CalendarDay__selected:active,
+.CalendarDay__selected:hover {
+ background-color: #484848;
+}
diff --git a/src/app/components/header/search-box/SearchBox.module.css b/src/app/components/header/search-box/SearchBox.module.css
new file mode 100644
index 0000000..70ca9ef
--- /dev/null
+++ b/src/app/components/header/search-box/SearchBox.module.css
@@ -0,0 +1,27 @@
+.search-box {
+ position: relative;
+ left: 20%;
+
+ display: inline-block;
+ width: 35%;
+ padding-left: 10px;
+ margin-top: 10px;
+
+ border-width: 0;
+ background: rgb(242, 244, 247);
+ box-shadow: inset 0 0 0 1px rgb(217, 219, 222);
+ font-size: 15px;
+ line-height: 2.2;
+}
+
+.light {
+ background: rgb(242, 244, 247);
+}
+
+.dark::placeholder {
+ color: rgb(207, 207, 207);
+}
+
+.dark {
+ background: rgb(125, 125, 125);
+}
diff --git a/src/app/components/header/search-box/SearchBox.tsx b/src/app/components/header/search-box/SearchBox.tsx
new file mode 100644
index 0000000..910de2c
--- /dev/null
+++ b/src/app/components/header/search-box/SearchBox.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import styles from './SearchBox.module.css';
+import {ThemeContext, themes} from "../../../../theme/theme-context";
+
+export class SearchBox extends React.Component {
+ render() {
+ const colorStyle = this.context === themes.light ? styles.light : styles.dark;
+ return ;
+ }
+}
+
+SearchBox.contextType = ThemeContext;
\ No newline at end of file
diff --git a/src/app/components/header/themed-button/ThemedButton.module.css b/src/app/components/header/themed-button/ThemedButton.module.css
new file mode 100644
index 0000000..c020cda
--- /dev/null
+++ b/src/app/components/header/themed-button/ThemedButton.module.css
@@ -0,0 +1,17 @@
+.button {
+ position: absolute;
+ top: 15px;
+ right: 20px;
+ border-radius: 3px;
+ font-size: 15px;
+}
+
+.dark {
+ background-color: white;
+ color: #2b2c2b;
+}
+
+.light {
+ background-color: #2b2c2b;
+ color: white;
+}
diff --git a/src/app/components/header/themed-button/ThemedButton.tsx b/src/app/components/header/themed-button/ThemedButton.tsx
new file mode 100644
index 0000000..d2a7976
--- /dev/null
+++ b/src/app/components/header/themed-button/ThemedButton.tsx
@@ -0,0 +1,24 @@
+import React from 'react'
+import {ThemeContext, themes} from "../../../../theme/theme-context";
+import styles from './ThemedButton.module.css';
+
+interface IProps {
+ changeTheme: () => void
+}
+
+class ThemedButton extends React.Component {
+
+ render() {
+ let theme = this.context;
+ const buttonColor = theme === themes.light ? styles.light : styles.dark;
+ return (
+
+ );
+ }
+}
+
+ThemedButton.contextType = ThemeContext;
+
+export default ThemedButton;
\ No newline at end of file
diff --git a/src/app/components/main-block/MainBlock.module.css b/src/app/components/main-block/MainBlock.module.css
new file mode 100644
index 0000000..4e89e85
--- /dev/null
+++ b/src/app/components/main-block/MainBlock.module.css
@@ -0,0 +1,20 @@
+.main-block {
+ position: absolute;
+
+ display: inline-block;
+
+ width: calc(100% - 200px);
+ margin-top: 15px;
+ border-radius: 3px;
+ box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.4);
+}
+
+.dark {
+ background-color: #56545436;
+ box-shadow: 0 2px 6px 0 #e5eaf0;
+}
+
+.light {
+ background-color: #ffffff;
+ box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.4);
+}
diff --git a/src/app/components/main-block/MainBlock.tsx b/src/app/components/main-block/MainBlock.tsx
new file mode 100644
index 0000000..1cafc97
--- /dev/null
+++ b/src/app/components/main-block/MainBlock.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import styles from './MainBlock.module.css';
+import { TopBar } from './top-bar/TopBar';
+import { Footer } from './footer/Footer';
+import { MessagesBox } from './messages-box/MessagesBox';
+import { IMessage } from "../../app";
+import { ThemeContext, themes } from "../../../theme/theme-context";
+
+interface PropsType {
+ topBarCheckboxHandler: (isChecked: boolean) => void
+ deleteMessages: () => void
+ checkboxHandler: (id: string) => void
+ messages: IMessage[]
+}
+
+export class MainBlock extends React.Component {
+ render() {
+ const theme = this.context;
+ const colorStyle = theme === themes.light ? styles.light : styles.dark;
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+MainBlock.contextType = ThemeContext;
diff --git a/src/app/components/main-block/footer/Footer.module.css b/src/app/components/main-block/footer/Footer.module.css
new file mode 100644
index 0000000..86cf5da
--- /dev/null
+++ b/src/app/components/main-block/footer/Footer.module.css
@@ -0,0 +1,5 @@
+.footer {
+ width: 100%;
+
+ border-top: solid 1px #e2e2e2;
+}
diff --git a/src/app/components/main-block/footer/Footer.tsx b/src/app/components/main-block/footer/Footer.tsx
new file mode 100644
index 0000000..1235198
--- /dev/null
+++ b/src/app/components/main-block/footer/Footer.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import styles from './Footer.module.css';
+import { FooterBar } from './footer-bar/FooterBar';
+
+export class Footer extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+}
diff --git a/src/app/components/main-block/footer/footer-bar/FooterBar.module.css b/src/app/components/main-block/footer/footer-bar/FooterBar.module.css
new file mode 100644
index 0000000..e0c2b84
--- /dev/null
+++ b/src/app/components/main-block/footer/footer-bar/FooterBar.module.css
@@ -0,0 +1,20 @@
+.footer-bar {
+ position: relative;
+
+ margin-top: 8px;
+ margin-right: 20px;
+ margin-bottom: 8px;
+ float: right;
+}
+
+.item {
+ display: inline-block;
+ margin-left: 25px;
+}
+
+.link {
+ color: #9b9b9b;
+ cursor: pointer;
+ font-size: 11px;
+ text-decoration: none;
+}
diff --git a/src/app/components/main-block/footer/footer-bar/FooterBar.tsx b/src/app/components/main-block/footer/footer-bar/FooterBar.tsx
new file mode 100644
index 0000000..90f5e45
--- /dev/null
+++ b/src/app/components/main-block/footer/footer-bar/FooterBar.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import styles from './FooterBar.module.css';
+
+export class FooterBar extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/src/app/components/main-block/messages-box/MessagesBox.module.css b/src/app/components/main-block/messages-box/MessagesBox.module.css
new file mode 100644
index 0000000..1211a76
--- /dev/null
+++ b/src/app/components/main-block/messages-box/MessagesBox.module.css
@@ -0,0 +1,15 @@
+.messages-box {
+ position: relative;
+
+ display: block;
+ overflow: scroll;
+ height: 500px;
+}
+
+.dark {
+ color: white;
+}
+
+.light {
+ color: black;
+}
diff --git a/src/app/components/main-block/messages-box/MessagesBox.tsx b/src/app/components/main-block/messages-box/MessagesBox.tsx
new file mode 100644
index 0000000..17132f8
--- /dev/null
+++ b/src/app/components/main-block/messages-box/MessagesBox.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import styles from './MessagesBox.module.css';
+import { Message } from './message/Message';
+import { HiddenBox } from './hidden-box/HiddenBox';
+import {IMessage} from "../../../app";
+import {ThemeContext, themes} from "../../../../theme/theme-context";
+
+interface IProps {
+ messages: IMessage[]
+ checkboxHandler: (id: string) => void
+}
+
+interface IState {
+ messageText: string
+ opened: boolean
+}
+
+export class MessagesBox extends React.Component {
+ constructor(props: IProps) {
+ super(props);
+ this.state = {
+ messageText: '',
+ opened: false
+ };
+ this.openMessage = this.openMessage.bind(this);
+ this.closeMessage = this.closeMessage.bind(this);
+ }
+
+ openMessage(message: string) {
+ this.setState({ opened: true, messageText: message });
+ }
+
+ closeMessage() {
+ this.setState({ opened: false });
+ }
+
+ render() {
+ const colorStyle = this.context === themes.light ? styles.light : styles.dark;
+ return (
+
+ {this.state.opened === true ? (
+
+ ) : (
+
+ {this.props.messages.map(messageData => (
+
+ ))}
+
+ )}
+
+ );
+ }
+}
+
+MessagesBox.contextType = ThemeContext;
diff --git a/src/app/components/main-block/messages-box/hidden-box/HiddenBox.module.css b/src/app/components/main-block/messages-box/hidden-box/HiddenBox.module.css
new file mode 100644
index 0000000..f07d04e
--- /dev/null
+++ b/src/app/components/main-block/messages-box/hidden-box/HiddenBox.module.css
@@ -0,0 +1,20 @@
+.content {
+ min-height: 500px;
+ margin: 5px;
+}
+
+.dark {
+ color: white;
+}
+
+.light {
+ color: #1c1c1c;
+}
+
+.cancel-btn {
+ color: gray;
+ cursor: pointer;
+ float: right;
+ font-family: YandexSansThin, serif;
+ font-size: 30px;
+}
diff --git a/src/app/components/main-block/messages-box/hidden-box/HiddenBox.tsx b/src/app/components/main-block/messages-box/hidden-box/HiddenBox.tsx
new file mode 100644
index 0000000..39800cb
--- /dev/null
+++ b/src/app/components/main-block/messages-box/hidden-box/HiddenBox.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import styles from './HiddenBox.module.css';
+import {ThemeContext, themes} from "../../../../../theme/theme-context";
+interface IProps {
+ closeMessage: () => void
+ messageText: string
+}
+export class HiddenBox extends React.Component {
+ render() {
+ const fontStyle = this.context === themes.light ? styles.light : styles.dark;
+ return (
+
+
{
+ this.props.closeMessage();
+ }}
+ >
+ X
+
+
{this.props.messageText}
+
+ );
+ }
+}
+
+HiddenBox.contextType = ThemeContext;
diff --git a/src/app/components/main-block/messages-box/message/Message.module.css b/src/app/components/main-block/messages-box/message/Message.module.css
new file mode 100644
index 0000000..05cfa48
--- /dev/null
+++ b/src/app/components/main-block/messages-box/message/Message.module.css
@@ -0,0 +1,85 @@
+.message {
+ height: 0;
+ padding-top: 5px;
+ padding-bottom: 5px;
+
+ border-bottom: solid 1px #e2e2e2;
+ opacity: 0;
+ transition: opacity 1s, height 1s;
+}
+
+.sender-img {
+ display: inline-block;
+ width: 30px;
+ height: 30px;
+ margin-right: 10px;
+ margin-left: 15px;
+ background-color: #f33;
+ border-radius: 100px;
+ color: #fff;
+ line-height: 30px;
+ text-align: center;
+ vertical-align: middle;
+}
+
+.sender {
+ display: inline-block;
+ width: 250px;
+ margin-right: 10px;
+}
+
+.theme {
+ display: inline-block;
+ overflow: hidden;
+ width: calc(100% - 500px);
+ margin-right: 10px;
+ margin-left: 10px;
+ text-overflow: ellipsis;
+ vertical-align: text-bottom;
+ white-space: nowrap;
+}
+
+.date {
+ position: relative;
+ top: 5px;
+ right: 20px;
+
+ display: inline-block;
+ color: #9b9b9b;
+ float: right;
+ font-weight: normal;
+ vertical-align: middle;
+}
+
+.checkbox {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin-left: 2%;
+ vertical-align: middle;
+}
+
+.unread-circle {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ background: #6287bd;
+ border-radius: 50%;
+}
+
+.to-create {
+ height: 30px;
+ opacity: 1;
+}
+
+.to-delete {
+ height: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ opacity: 0;
+}
+
+.to-hide {
+ display: none;
+ height: 0;
+}
diff --git a/src/app/components/main-block/messages-box/message/Message.tsx b/src/app/components/main-block/messages-box/message/Message.tsx
new file mode 100644
index 0000000..90462d8
--- /dev/null
+++ b/src/app/components/main-block/messages-box/message/Message.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import styles from './Message.module.css';
+import {IMessage} from "../../../../app";
+
+interface IProps {
+ messageData: IMessage
+ checkboxHandler: (id: string) => void
+ openMessage: (message: string) => void
+}
+
+export class Message extends React.Component {
+ constructor(props: IProps) {
+ super(props);
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(e: React.ChangeEvent) {
+ const messageID = e.target.id;
+ this.props.checkboxHandler(messageID);
+ }
+
+ render() {
+ const { messageData } = this.props;
+ const animation =
+ (messageData.toCreate ? ` ${styles['to-create']}` : '') +
+ (messageData.toDelete ? ` ${styles['to-delete']}` : '');
+ return (
+ ) => {
+ if ((event.target as HTMLInputElement).className !== styles.checkbox) {
+ this.props.openMessage(messageData.text);
+ }
+ }}
+ >
+
+
{messageData.firstLetterSender}
+
{messageData.sender}
+
+
{messageData.theme}
+
{messageData.date.toLocaleDateString('ru-RU', { month: 'long', day: 'numeric' })}
+
+ );
+ }
+}
diff --git a/src/app/components/main-block/top-bar/TopBar.module.css b/src/app/components/main-block/top-bar/TopBar.module.css
new file mode 100644
index 0000000..5c08777
--- /dev/null
+++ b/src/app/components/main-block/top-bar/TopBar.module.css
@@ -0,0 +1,20 @@
+.top-bar {
+ position: relative;
+
+ display: inline-block;
+ width: 100%;
+ padding-top: 10px;
+ padding-bottom: 10px;
+
+ border-bottom: solid 1px #e2e2e2;
+}
+
+.checkbox {
+ position: relative;
+
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin-left: 2%;
+ float: left;
+}
diff --git a/src/app/components/main-block/top-bar/TopBar.tsx b/src/app/components/main-block/top-bar/TopBar.tsx
new file mode 100644
index 0000000..06a3b61
--- /dev/null
+++ b/src/app/components/main-block/top-bar/TopBar.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import styles from './TopBar.module.css';
+import { HorizontalNavPanel } from './horizontal-nav-panel/HorizontalNavPanel';
+
+interface IProps {
+ topBarCheckboxHandler: (val: boolean) => void
+ deleteMessages: () => void
+}
+
+interface IState {
+ topBarCheckboxChecked: boolean
+}
+
+export class TopBar extends React.Component {
+ constructor(props: IProps) {
+ super(props);
+ this.state = {
+ topBarCheckboxChecked: false
+ };
+ this.handleChangeTopBarCheckbox = this.handleChangeTopBarCheckbox.bind(this);
+ }
+
+ handleChangeTopBarCheckbox(e: React.ChangeEvent) {
+ const isChecked = e.target.checked;
+ this.props.topBarCheckboxHandler(isChecked);
+ this.setState({ topBarCheckboxChecked: isChecked });
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
diff --git a/src/app/components/main-block/top-bar/horizontal-nav-panel/HorizontalNavPanel.module.css b/src/app/components/main-block/top-bar/horizontal-nav-panel/HorizontalNavPanel.module.css
new file mode 100644
index 0000000..25fac20
--- /dev/null
+++ b/src/app/components/main-block/top-bar/horizontal-nav-panel/HorizontalNavPanel.module.css
@@ -0,0 +1,38 @@
+.horizontal-nav-panel {
+ position: relative;
+ left: 20px;
+
+ margin: auto;
+ list-style-type: none;
+}
+
+.item {
+ margin-right: 25px;
+ float: left;
+}
+
+.button {
+ border-style: none;
+ background-color: #fff;
+ color: #ccc;
+ cursor: pointer;
+ font: 13px 'Helvetica Neue', sans-serif;
+ outline: none;
+}
+
+.link {
+ display: block;
+ padding: 3px;
+ color: #707070;
+ text-decoration: none;
+}
+
+.dark {
+ background-color: #2b2c2b;
+ color: white;
+}
+
+.light {
+ background-color: white;
+ color: #707070;
+}
diff --git a/src/app/components/main-block/top-bar/horizontal-nav-panel/HorizontalNavPanel.tsx b/src/app/components/main-block/top-bar/horizontal-nav-panel/HorizontalNavPanel.tsx
new file mode 100644
index 0000000..d7776ac
--- /dev/null
+++ b/src/app/components/main-block/top-bar/horizontal-nav-panel/HorizontalNavPanel.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import styles from './HorizontalNavPanel.module.css';
+import {ThemeContext, themes} from "../../../../../theme/theme-context";
+
+interface IProps {
+ deleteMessages: () => void
+}
+
+export class HorizontalNavPanel extends React.Component {
+ createNavigationPanelItem = (name: string, onClickFunction: (() => void) | undefined, colorStyle: string) => {
+ return (
+
+
+
+ );
+ };
+
+ render() {
+ const navigationPanelValues = [
+ { name: 'Переслать', function: undefined },
+ { name: 'Удалить', function: this.props.deleteMessages },
+ { name: 'Это спам!', function: undefined },
+ { name: 'Прочитано', function: undefined }
+ ];
+ const theme = this.context;
+ const colorStyle = theme === themes.light ? styles.light : styles.dark;
+ return (
+
+ {navigationPanelValues.map(element =>
+ this.createNavigationPanelItem(element.name, element.function, colorStyle)
+ )}
+
+ );
+ }
+}
+
+HorizontalNavPanel.contextType = ThemeContext;
diff --git a/src/app/components/menu/Menu.module.css b/src/app/components/menu/Menu.module.css
new file mode 100644
index 0000000..e76d42f
--- /dev/null
+++ b/src/app/components/menu/Menu.module.css
@@ -0,0 +1,29 @@
+.menu {
+ display: inline-block;
+
+ width: 150px;
+ height: 100vh;
+ margin-top: 15px;
+ margin-left: 22px;
+}
+
+.button {
+ width: 90%;
+ height: 35px;
+ border-radius: 4px;
+ color: #ffffff;
+ cursor: pointer;
+ font-size: 12px;
+}
+
+.dark {
+ border-color: #54585e;
+ background-color: #54585e;
+ box-shadow: 0 2px 4px #54585e;
+}
+
+.light {
+ border-color: #6287bd;
+ background-color: #6287bd;
+ box-shadow: 0 2px 4px #6287bd;
+}
diff --git a/src/app/components/menu/Menu.tsx b/src/app/components/menu/Menu.tsx
new file mode 100644
index 0000000..18b9399
--- /dev/null
+++ b/src/app/components/menu/Menu.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import styles from './Menu.module.css';
+import { MenuItems } from './menu-items/MenuItems';
+import {ThemeContext, themes} from "../../../theme/theme-context";
+
+interface IProps {
+ newMail: () => void
+}
+
+export class Menu extends React.Component {
+ render() {
+ const buttonColorStyle = this.context === themes.light ? styles.light : styles.dark;
+ return (
+
+
+
+
+ );
+ }
+}
+
+Menu.contextType = ThemeContext;
diff --git a/src/app/components/menu/menu-items/MenuItems.module.css b/src/app/components/menu/menu-items/MenuItems.module.css
new file mode 100644
index 0000000..634f21b
--- /dev/null
+++ b/src/app/components/menu/menu-items/MenuItems.module.css
@@ -0,0 +1,54 @@
+.items {
+ position: relative;
+
+ width: 90%;
+ height: 22px;
+ padding: 0;
+ font-family: 'Helvetica Neue', serif;
+ font-size: 11px;
+ line-height: 20px;
+ list-style-type: none;
+ vertical-align: middle;
+}
+
+.item-light {
+ padding-left: 5px;
+ border-radius: 3px;
+}
+
+.item_active-light {
+ background-color: #cdd6e4;
+ font-family: YandexSansBold, sans-serif;
+}
+
+.item-active-dark {
+ background-color: #666768;
+ font-family: YandexSansBold, sans-serif;
+}
+
+.item-light:hover {
+ background-color: #cdd6e4;
+}
+
+.item-dark {
+ padding-left: 5px;
+ border-radius: 3px;
+}
+
+.item-dark:hover {
+ background-color: #666768;
+}
+
+.link {
+ display: block;
+ padding: 3px;
+ text-decoration: none;
+}
+
+.font-light {
+ color: #707070;
+}
+
+.font-dark {
+ color: white;
+}
diff --git a/src/app/components/menu/menu-items/MenuItems.tsx b/src/app/components/menu/menu-items/MenuItems.tsx
new file mode 100644
index 0000000..741553f
--- /dev/null
+++ b/src/app/components/menu/menu-items/MenuItems.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import styles from './MenuItems.module.css';
+import {ThemeContext, themes} from "../../../../theme/theme-context";
+
+export class MenuItems extends React.Component {
+ createMenuItem = (name: string, fontStyle: string, itemStyle: string) => {
+ return (
+
+
+ {name}
+
+
+ );
+ };
+
+ render() {
+ const menuItemsNames = ['Отправленные', 'Удаленные', 'Спам', 'Черновики', 'Создать папку'];
+ const theme = this.context;
+ const fontStyle = theme === themes.light ? styles["font-light"] : styles["font-dark"];
+ const itemActiveStyle = theme === themes.light ? styles["item_active-light"] : styles["item-active-dark"];
+ const itemStyle = theme === themes.light ? styles["item-light"] : styles["item-dark"];
+ return (
+
+ -
+
+ Входящие
+
+
+ {menuItemsNames.map(name => this.createMenuItem(name, fontStyle, itemStyle))}
+
+ );
+ }
+}
+
+MenuItems.contextType = ThemeContext;
diff --git a/src/app/message-templates.ts b/src/app/message-templates.ts
new file mode 100644
index 0000000..a7cf062
--- /dev/null
+++ b/src/app/message-templates.ts
@@ -0,0 +1,17 @@
+const senders = ['Championat.com', 'Sportbox', 'Матч ТВ', 'Eurosport'];
+
+export function getRandomIndex(arraySize: number) {
+ return Math.floor(Math.random() * arraySize);
+}
+
+export function getRandomSender() {
+ return senders[getRandomIndex(senders.length)];
+}
+
+export async function getRandomThemeAndText() {
+ const responseText = await fetch('https://baconipsum.com/api/?type=meat-and-filler');
+ const responseTheme = await fetch('https://baconipsum.com/api/?type=meat-and-filler&sentences=1');
+ const dataText = await responseText.json();
+ const dataTheme = await responseTheme.json();
+ return [dataTheme[0], dataText[0]];
+}
diff --git a/src/index.css b/src/index.css
index 2b6e525..ed6cf04 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,8 +1,11 @@
+@import url('./resources/fonts/fontsStyle.css');
+
body {
- padding: 0;
margin: 0;
+ font-family: 'Helvetica Neue', sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+ font-size: 13px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
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/fonts/YandexSansText-Bold.ttf b/src/resources/fonts/YandexSansText-Bold.ttf
new file mode 100644
index 0000000..e267b3f
Binary files /dev/null and b/src/resources/fonts/YandexSansText-Bold.ttf differ
diff --git a/src/resources/fonts/YandexSansText-Thin.ttf b/src/resources/fonts/YandexSansText-Thin.ttf
new file mode 100644
index 0000000..3ac9b03
Binary files /dev/null and b/src/resources/fonts/YandexSansText-Thin.ttf differ
diff --git a/src/resources/fonts/fontsStyle.css b/src/resources/fonts/fontsStyle.css
new file mode 100644
index 0000000..6671886
--- /dev/null
+++ b/src/resources/fonts/fontsStyle.css
@@ -0,0 +1,9 @@
+@font-face {
+ font-family: YandexSansThin;
+ src: url('./YandexSansText-Thin.ttf');
+}
+
+@font-face {
+ font-family: YandexSansBold;
+ src: url('./YandexSansText-Bold.ttf');
+}
diff --git a/src/resources/images/yandex-mail-dark.png b/src/resources/images/yandex-mail-dark.png
new file mode 100644
index 0000000..d2b1bc7
Binary files /dev/null and b/src/resources/images/yandex-mail-dark.png differ
diff --git a/src/resources/images/yandex-mail-light.png b/src/resources/images/yandex-mail-light.png
new file mode 100644
index 0000000..fcdb233
Binary files /dev/null and b/src/resources/images/yandex-mail-light.png differ
diff --git a/src/theme/theme-context.ts b/src/theme/theme-context.ts
new file mode 100644
index 0000000..84ef2e1
--- /dev/null
+++ b/src/theme/theme-context.ts
@@ -0,0 +1,12 @@
+import React from "react";
+
+export enum themes {
+ light = 'light',
+ dark = 'dark',
+}
+
+export const ThemeContext = React.createContext(
+ themes.light
+);
+
+export const ThemeProvider = ThemeContext.Provider;
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..2f4d2c3
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "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"]
+}