diff --git a/package.json b/package.json
index ef9fa82..97ac06b 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,16 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "axios": "^0.18.0",
+ "immutable": "^3.8.2",
"react": "^16.2.0",
"react-dom": "^16.2.0",
- "react-scripts": "1.1.0"
+ "react-redux": "^5.1.0",
+ "react-scripts": "1.1.0",
+ "redux": "^4.0.1",
+ "redux-immutable": "^4.0.0",
+ "reselect": "^4.0.0",
+ "styled-components": "^4.0.2"
},
"scripts": {
"start": "react-scripts start",
@@ -13,4 +20,4 @@
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
-}
\ No newline at end of file
+}
diff --git a/src/App.css b/src/App.css
index 43515ed..8fbcb6a 100644
--- a/src/App.css
+++ b/src/App.css
@@ -9,17 +9,32 @@ body,
padding: 0;
}
+body * {
+ box-sizing: border-box;
+}
+
.app {
display: flex;
flex-direction: column;
+ position: relative;
+ padding-bottom: 40px;
}
.chatArea {
flex-grow: 1;
+ flex: 1;
+ overflow: auto;
margin: 10px;
border: 1px solid #ccc;
background-color: #fafafa;
}
+.chatArea > p {
+ padding: 12px;
+ margin: 0;
+}
+.chatArea > p:nth-of-type(even) {
+ background: #eee;
+}
.messageBar {
display: flex;
diff --git a/src/App.js b/src/App.js
index 932a66d..78f066f 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,17 +1,114 @@
import React, { Component } from "react";
-import MessageBar from "./MessageBar";
-import ChatArea from "./ChatArea";
+import { connect } from 'react-redux';
+import axios from 'axios';
+
+import { commands } from './constants';
+import MessageBar from "./components/MessageBar";
+import ChatArea from "./components/ChatArea";
+import ErrorMessage from "./components/ErrorMessage";
import "./App.css";
+import { IDGenerator } from './utils';
+import { addMessage, updateMessage, stopApplication } from './actions';
class App extends Component {
- render() {
- return (
-
-
-
-
- );
+ constructor(props) {
+ super(props);
+ this.state = {
+ errorMessage: ''
+ }
+ }
+ searchForCharacter = (messageId, query) => {
+ axios.get(`https://swapi.co/api/people/?search=${query}`).then(data => {
+ if(data) {
+ const { results } = data.data;
+ if (Array.isArray(results) && results.length) {
+ this.props.updateMessage(messageId, `Search results for \`${query}\`: ${results[0].name}`);
+ }
+ else {
+ this.props.updateMessage(messageId, `No results for \`${query}\``);
+ }
+ }
+ }, err => {
+ this.props.updateMessage(messageId, `There is an error when searching \`${query}\``);
+ });
+ }
+ messageConverter = (message) => {
+ let messageResults = null;
+ const messageArray = message.split(' ');
+ const messageCommand = messageArray.shift();
+ const id = IDGenerator();
+
+ const firstChar = messageCommand[0];
+ if (firstChar === '/') {
+ message = messageCommand.substring(1);
+ if (commands.indexOf(message) > -1) {
+ switch(message) {
+ case 'time':
+ messageResults = `Current time: ${new Date().toString()}`;
+ break;
+ case 'starwars':
+ const query = messageArray.join(' ');
+ if (!query.replace(/ /g,'')) {
+ messageResults = null;
+ this.setState({ errorMessage: 'Please enter a query' });
+ break;
+ }
+ this.searchForCharacter(id, query);
+ messageResults = `Searching character by name: ${query}`;
+ break;
+ case 'goodbye':
+ this.props.stopApplication();
+ messageResults = 'Bye! See you again.';
+ break;
+ default:
+ messageResults = message;
+ break;
+ }
+ }
+ else {
+ messageResults = `Command \`${message}\` does not exist.`;
+ }
+ }
+ else {
+ messageResults = message;
+ }
+ if (!messageResults) return null;
+ return { id, content: messageResults };
+ }
+ onSubmit = (message) => {
+ this.setState({ errorMessage: '' });
+ const finalMessage = this.messageConverter(message);
+ if (finalMessage) {
+ this.props.addMessage(finalMessage);
+ return true;
}
+ return false;
+
+ }
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ addMessage(message) {
+ dispatch(addMessage(message));
+ },
+ updateMessage(messageId, content) {
+ dispatch(updateMessage(messageId, content));
+ },
+ stopApplication() {
+ dispatch(stopApplication());
+ },
+ };
}
-export default App;
+export default connect(null, mapDispatchToProps)(App);
+
diff --git a/src/ChatArea.js b/src/ChatArea.js
deleted file mode 100644
index c4e314c..0000000
--- a/src/ChatArea.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import React, { Component } from "react";
-
-class ChatArea extends Component {
- render() {
- return ;
- }
-}
-
-export default ChatArea;
diff --git a/src/MessageBar.js b/src/MessageBar.js
deleted file mode 100644
index 05a6028..0000000
--- a/src/MessageBar.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React, { Component } from "react";
-import TextInput from "./TextInput";
-import SendButton from "./SendButton";
-
-class MessageBar extends Component {
- render() {
- return (
-
-
-
-
- );
- }
-}
-
-export default MessageBar;
diff --git a/src/SendButton.js b/src/SendButton.js
deleted file mode 100644
index dd4ae98..0000000
--- a/src/SendButton.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import React, { Component } from "react";
-
-class SendButton extends Component {
- render() {
- return ;
- }
-}
-
-export default SendButton;
diff --git a/src/TextInput.js b/src/TextInput.js
deleted file mode 100644
index 4b2acd0..0000000
--- a/src/TextInput.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import React, { Component } from "react";
-
-class MessageBar extends Component {
- render() {
- return (
-
-
-
- );
- }
-}
-
-export default MessageBar;
diff --git a/src/actions.js b/src/actions.js
new file mode 100644
index 0000000..db98bcb
--- /dev/null
+++ b/src/actions.js
@@ -0,0 +1,11 @@
+import { STOP_APPLICATION, ADD_MESSAGE, UPDATE_MESSAGE } from './constants';
+
+export function stopApplication() {
+ return { type: STOP_APPLICATION };
+}
+export function addMessage(message) {
+ return { type: ADD_MESSAGE, message };
+}
+export function updateMessage(messageId, content) {
+ return { type: UPDATE_MESSAGE, messageId, content };
+}
diff --git a/src/components/ChatArea.js b/src/components/ChatArea.js
new file mode 100644
index 0000000..1e63dcc
--- /dev/null
+++ b/src/components/ChatArea.js
@@ -0,0 +1,39 @@
+import React, { Component } from "react";
+import PropTypes from 'prop-types';
+import { createStructuredSelector } from 'reselect';
+import { connect } from 'react-redux';
+import { selectApplicationStopped, selectMessages } from '../selectors';
+import { stopApplication } from '../actions';
+
+class ChatArea extends Component {
+ render() {
+ const { messages } = this.props;
+ if (typeof messages === 'undefined') return null;
+ const messagesDisplay = messages.map((item, itemIndex) => { item.content }
)
+ return
+ { messagesDisplay }
+
+ }
+}
+
+ChatArea.defaultProps = {
+ contents: []
+};
+
+ChatArea.propTypes = {
+ contents: PropTypes.array
+};
+
+function mapDispatchToProps(dispatch) {
+ return {
+ stopApplication() {
+ dispatch(stopApplication());
+ }
+ };
+}
+
+const mapStateToProps = createStructuredSelector({
+ applicationStopped: selectApplicationStopped(),
+ messages: selectMessages()
+});
+export default connect(mapStateToProps, mapDispatchToProps)(ChatArea);
diff --git a/src/components/ErrorMessage.js b/src/components/ErrorMessage.js
new file mode 100644
index 0000000..1bd4db0
--- /dev/null
+++ b/src/components/ErrorMessage.js
@@ -0,0 +1,25 @@
+import React from "react";
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+
+const StyledMessage = styled.div`
+ color: red;
+ font-size: 12px;
+ padding-left: 12px;
+ position: absolute;
+ bottom: 30px;
+`;
+const ErrorMessage = props => {
+ const { message } = props;
+ return { message }
+}
+
+ErrorMessage.defaultProps = {
+ message: ' '
+};
+
+ErrorMessage.propTypes = {
+ message: PropTypes.string
+};
+export default ErrorMessage;
+
diff --git a/src/components/MessageBar.js b/src/components/MessageBar.js
new file mode 100644
index 0000000..c259e7f
--- /dev/null
+++ b/src/components/MessageBar.js
@@ -0,0 +1,88 @@
+import React, { Component } from "react";
+import styled from 'styled-components';
+import { connect } from 'react-redux';
+import { createStructuredSelector } from 'reselect';
+import PropTypes from 'prop-types';
+
+import { selectApplicationStopped } from '../selectors';
+import { addMessage } from '../actions';
+
+import SuggestedCommands from './SuggestedCommands';
+
+const StyledWrapper = styled.form`
+ display: flex;
+ position: relative;
+`;
+const StyledInput = styled.input`
+ height: 34px;
+ padding: 0 12px;
+ flex: 1;
+ margin-right: 15px;
+`;
+
+const StyledButton = styled.button`
+ height: 34px;
+ padding: 0 12px;
+`;
+
+class MessageBar extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ message: '',
+ commandOpened: false
+ }
+ };
+
+ onMessageChange = (evt) => {
+ const message = evt.target.value;
+ const commandOpened = message[0] === '/';
+ this.setState({ message, commandOpened });
+ };
+ onSubmit = (evt) => {
+ evt.preventDefault();
+ const { onSubmit } = this.props;
+ var result = onSubmit(this.state.message);
+ if (result) {
+ this.setState({ message: '', commandOpened: false });
+ }
+ }
+
+ onCommandSelect = (command) => {
+ this.setState({ message: `/${command}`, commandOpened: false });
+ this.messageInput.focus();
+ }
+
+ messageInput = null;
+
+ render() {
+ const { message, commandOpened } = this.state;
+ const { applicationStopped } = this.props;
+
+ return (
+
+ { this.messageInput = el; }} />
+ { !applicationStopped ? SEND : null }
+
+
+ );
+ }
+}
+
+MessageBar.propTypes = {
+ onSubmit: PropTypes.func,
+};
+
+function mapDispatchToProps(dispatch) {
+ return {
+ addMessage(message) {
+ dispatch(addMessage(message));
+ }
+ };
+}
+const mapStateToProps = createStructuredSelector({
+ applicationStopped: selectApplicationStopped()
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(MessageBar);
+
diff --git a/src/components/SuggestedCommands.js b/src/components/SuggestedCommands.js
new file mode 100644
index 0000000..80d8990
--- /dev/null
+++ b/src/components/SuggestedCommands.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+
+import { commands } from '../constants';
+
+const StyledWrapper = styled.ul`
+ position: absolute;
+ margin: -10px 0 0 0;
+ padding: 8px 0;
+ border-radius: 4px;
+ top: 0;
+ transform: translateY(-100%);
+ background: #eee;
+ box-shadow: 0px 5px 17px 0px rgba(0,0,0,.2);
+ min-width: 120px;
+
+ li {
+ list-style: none;
+ padding: 4px 12px;
+ cursor: pointer;
+ &:hover {
+ background: #333;
+ color: #fff;
+ }
+ }
+`;
+
+class SuggestedCommands extends React.Component {
+
+ onCommandSelect = (item) => {
+ const { onCommandSelect } = this.props;
+ if (typeof onCommandSelect === 'function') {
+ if (item === 'starwars') item += ' ';
+ onCommandSelect(item);
+ }
+ }
+ renderCommand = () => {
+ return commands.map((item) => { this.onCommandSelect(item); }} key={`command-${item}`}>{ item })
+ }
+ render() {
+ const { open } = this.props;
+ return !open ? null :
+ { this.renderCommand() }
+
+ }
+}
+
+SuggestedCommands.propTypes = {
+ message: PropTypes.string,
+ open: PropTypes.bool,
+ onCommandSelect: PropTypes.func,
+};
+
+export default SuggestedCommands;
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 0000000..b03aeef
--- /dev/null
+++ b/src/constants.js
@@ -0,0 +1,5 @@
+export const STOP_APPLICATION = 'soloChat/STOP_APPLICATION';
+export const ADD_MESSAGE = 'soloChat/ADD_MESSAGE';
+export const UPDATE_MESSAGE = 'soloChat/UPDATE_MESSAGE';
+
+export const commands = ['time', 'starwars', 'goodbye'];
diff --git a/src/index.js b/src/index.js
index c24e9d8..e3504b6 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,5 +1,16 @@
import React from "react";
import ReactDOM from "react-dom";
+import { Provider } from 'react-redux'
+import { createStore } from 'redux'
+import MainReducers from './reducers';
+
import App from "./App";
-ReactDOM.render(, document.getElementById("root"));
+const store = createStore(MainReducers);
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById("root")
+);
diff --git a/src/reducers.js b/src/reducers.js
new file mode 100644
index 0000000..0bbd3a9
--- /dev/null
+++ b/src/reducers.js
@@ -0,0 +1,33 @@
+import { fromJS } from 'immutable';
+import { STOP_APPLICATION, ADD_MESSAGE, UPDATE_MESSAGE } from './constants';
+
+import Immutable from 'immutable';
+const initialState = Immutable.fromJS({
+ applicationStopped: false,
+ messages: []
+});
+
+const MainReducer = (state = initialState, action) => {
+ const messages = state.get('messages').toJS();
+ switch (action.type) {
+ case STOP_APPLICATION:
+ return state
+ .set('applicationStopped', true);
+ case ADD_MESSAGE:
+ messages.push(action.message);
+ return state
+ .set('messages', fromJS(messages));
+ case UPDATE_MESSAGE:
+ messages.forEach(mess => {
+ if (mess.id === action.messageId) {
+ mess.content = action.content;
+ }
+ });
+ return state
+ .set('messages', fromJS(messages));
+
+ default: return state;
+ }
+};
+
+export default MainReducer;
\ No newline at end of file
diff --git a/src/selectors.js b/src/selectors.js
new file mode 100644
index 0000000..2e349bc
--- /dev/null
+++ b/src/selectors.js
@@ -0,0 +1,16 @@
+import { createSelector } from 'reselect';
+
+const selectGlobal = () => state => state;
+const selectApplicationStopped = () => createSelector(
+ selectGlobal(),
+ state => (state ? state.get('applicationStopped') : false),
+);
+
+const selectMessages = () => createSelector(
+ selectGlobal(),
+ state => (state ? state.get('messages').toJS() : []),
+);
+export {
+ selectApplicationStopped,
+ selectMessages
+}
\ No newline at end of file
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 0000000..9f377f7
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,17 @@
+export const IDGenerator = () => {
+ const _getRandomInt = ( min, max ) => {
+ return Math.floor( Math.random() * ( max - min + 1 ) ) + min;
+ }
+
+ const timestamp = +new Date();
+ const length = 8;
+ const ts = timestamp.toString();
+ const parts = ts.split( "" ).reverse();
+ let id = "";
+
+ for( let i = 0; i < length; ++i ) {
+ const index = _getRandomInt( 0, parts.length - 1 );
+ id += parts[index];
+ }
+ return id;
+}
\ No newline at end of file