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 ( -
-
-

- 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..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 ( +
+ + yandex + +
+ 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"] +}