Browse Source

Merge branch 'release/v2.0'
A major release that has functioning components.

Snow 8 years ago
parent
commit
80c8b79105

+ 11 - 1
.gitignore

@@ -22,4 +22,14 @@ yarn-error.log*
 
 # sublime project staff
 ui.sublime-project
-ui.sublime-workspace
+ui.sublime-workspace
+
+# eslint
+.eslintrc.json
+
+# temp files
+\#
+\#*\#
+
+# tern
+.tern-port

File diff suppressed because it is too large
+ 15 - 1897
README.md


+ 26 - 4
package.json

@@ -1,20 +1,42 @@
 {
   "name": "common-ui-components",
-  "version": "0.1.0",
+  "version": "2.0.0",
   "private": true,
   "dependencies": {
+    "prop-types": "^15.5.10",
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
-    "react-router": "^4.1.1",
-    "react-motion": "^0.5.0"
+    "react-flip-move": "2.9.14",
+    "react-motion": "^0.5.0",
+    "react-redux": "^5.0.5",
+    "react-router-dom": "4.1.1",
+    "redux": "^3.7.0"
   },
   "devDependencies": {
+    "enzyme": "^2.8.2",
+    "eslint": "^3.19.0",
+    "eslint-config-airbnb": "^15.0.1",
+    "eslint-config-google": "^0.8.0",
+    "eslint-plugin-import": "^2.3.0",
+    "eslint-plugin-jsx-a11y": "^5.0.3",
+    "eslint-plugin-react": "^7.1.0",
     "react-scripts": "1.0.7"
   },
   "scripts": {
     "start": "react-scripts start",
     "build": "react-scripts build",
+    "precommit": "lint-staged",
     "test": "react-scripts test --env=jsdom",
     "eject": "react-scripts eject"
+  },
+  "lint-staged": {
+    "*.js": [
+      "prettier --single-quote --es5 --write",
+      "git add"
+    ],
+    "*.jsx": [
+      "prettier --single-quote --es5 --write",
+      "git add"
+    ]
   }
-}
+}

+ 0 - 8
src/App.test.js

@@ -1,8 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import App from './App';
-
-it('renders without crashing', () => {
-  const div = document.createElement('div');
-  ReactDOM.render(<App />, div);
-});

+ 31 - 0
src/__test__/Counter.test.js

@@ -0,0 +1,31 @@
+import React from 'react';
+import Counter from '../components/Counter/Counter';
+import { mount } from 'enzyme';
+
+// state in redux app is preserved among tests
+
+describe('<Counter />', () => {
+  const setup = () => {
+    const counter = mount(<Counter />);
+    return { counter };
+  };
+
+  it('Clicking on add increment the counter', () => {
+    const { counter } = setup();
+    counter.find('button').at(0).simulate('click').simulate('click');
+    expect(counter.find('StaticCounter').props().value).toBe(2);
+  });
+
+  it('Clicking on minus decrement the counter', () => {
+    const { counter } = setup();
+    counter.find('button').at(1).simulate('click');
+    expect(counter.find('StaticCounter').props().value).toBe(1);
+  });
+
+  it('Clicking on minus never goes below 0', () => {
+    const { counter } = setup();
+    const minusButton = counter.find('button').at(1);
+    minusButton.simulate('click').simulate('click');
+    expect(counter.find('StaticCounter').props().value).toBe(0);
+  });
+});

+ 85 - 0
src/__test__/Form.test.js

@@ -0,0 +1,85 @@
+import React from 'react';
+import Form from '../components/Form/Form.js';
+import { mount, shallow } from 'enzyme';
+
+describe('Place holder', () => {
+  const colorTest = (inputWindow, color) => {
+    const inputField = inputWindow.find('input').at(0).prop('style');
+    expect(inputField).toHaveProperty('color', color);
+  };
+
+  it('Placeholder shows on loading and color is grey', () => {
+    const inputWindow = shallow(<Form />);
+    expect(inputWindow.find('input').at(0).prop('value')).toBeTruthy();
+    colorTest(inputWindow, 'grey');
+  });
+
+  it('Clicking on the input field hides the placeholder', () => {
+    const inputWindow = mount(<Form />);
+    const inputField = inputWindow.find('input').at(0);
+    inputField.simulate('focus');
+    expect(inputField.prop('value')).toBe('');
+  });
+
+  it('Typing in the input area shows the text with color black, \
+ deleting the text shows placeholder again on blur', () => {
+    const inputWindow = mount(<Form />);
+    const inputField = inputWindow.find('input').at(0);
+    const placeHolder = inputField.prop('value');
+    const name = inputField.prop('name');
+    inputField.simulate('focus');
+    expect(inputField.prop('value')).toBe('');
+    const newValue = 'Entered new value';
+    inputField.simulate('change', {
+      target: {
+        name: name,
+        value: newValue
+      }
+    });
+    expect(inputField.prop('value')).toBe(newValue);
+    colorTest(inputWindow, 'black');
+    inputField.simulate('change', {
+      target: {
+        name: name,
+        value: ''
+      }
+    });
+    inputField.simulate('blur');
+    expect(inputField.prop('value')).toBe(placeHolder);
+    colorTest(inputWindow, 'grey');
+  });
+});
+
+describe('Validation', () => {
+  it('Not meeting the min length shows a warning on blur', () => {
+    const formWindow = mount(<Form />);
+    const inputField = formWindow.find('input').at(0);
+    const name = inputField.prop('name');
+    const newValue = 'test';
+    inputField.simulate('change', {
+      target: { name: name, value: newValue }
+    });
+    inputField.simulate('blur');
+    expect(formWindow.find('.warning').at(0).length).toBe(1);
+  });
+
+  it('Unmatched password shows a warning on blur', () => {
+    const formWindow = mount(<Form />);
+    const inputField = formWindow.find('input').at(1);
+    expect(inputField.prop('type')).toBe('password');
+    const name = inputField.prop('name');
+    const newValue = 'test';
+    inputField.simulate('change', {
+      target: { name: name, value: newValue }
+    });
+    inputField.simulate('blur');
+    expect(formWindow.find('.warning').length).toBeTruthy();
+  });
+
+  it('Submitting an empty form shows all warnings', () => {
+    const formWindow = mount(<Form />);
+    formWindow.simulate('submit');
+    // two warnings on two pwd input fields
+    expect(formWindow.find('.warning').length).toBeTruthy();
+  });
+});

+ 19 - 0
src/__test__/Modal.test.js

@@ -0,0 +1,19 @@
+import React from 'react';
+import Modal from '../components/Modal/Modal';
+import { mount, shallow } from 'enzyme';
+
+describe('<Modal />', () => {
+  it('Clicking show button shows a new node with class named "modal"', () => {
+    const modal = mount(<Modal />);
+    expect(modal.find('.modal').exists()).toBe(false);
+    modal.find('button.show').simulate('click');
+    expect(modal.find('.modal').exists()).toBe(true);
+  });
+
+  it('Clicking the close button close the modal window and make the class inactive', () => {
+    const modal = mount(<Modal />);
+    modal.find('button.show').simulate('click');
+    modal.find('button.hide').simulate('click');
+    expect(modal.find('.modal').exists()).toBe(false);
+  });
+});

+ 28 - 0
src/__test__/SlideShow.test.js

@@ -0,0 +1,28 @@
+import React from 'react';
+import SlideShow from '../components/SlideShow/';
+import { mount } from 'enzyme';
+
+describe('<SlideShow />', () => {
+  it('The SlideShow has 5 slides', () => {
+    const SlideShowWindow = mount(<SlideShow />);
+    expect(SlideShowWindow.find('.slide').length).toBe(5);
+  });
+  it('Clicking on buttons does not change the center position', () => {
+    const SlideShowWindow = mount(<SlideShow />);
+    expect(SlideShowWindow.find('.slide').at(2).hasClass('center')).toBe(true);
+    SlideShowWindow.find('button.right').simulate('click');
+    expect(SlideShowWindow.find('.slide').at(2).hasClass('center')).toBe(true);
+    SlideShowWindow.find('button.left').simulate('click');
+    expect(SlideShowWindow.find('.slide').at(2).hasClass('center')).toBe(true);
+  });
+
+  it('Clicking on buttons add z-index style to the corresponding box', () => {
+    const SlideShowWindow = mount(<SlideShow />);
+    SlideShowWindow.find('button.left').simulate('click');
+    expect(SlideShowWindow.find('.slide4').props().style.zIndex).toBeTruthy();
+    expect(SlideShowWindow.find('.slide0').props().style.zIndex).toBeFalsy();
+    SlideShowWindow.find('button.right').simulate('click');
+    expect(SlideShowWindow.find('.slide0').props().style.zIndex).toBeTruthy();
+    expect(SlideShowWindow.find('.slide4').props().style.zIndex).toBeFalsy();
+  });
+});

+ 55 - 0
src/__test__/TabView.test.js

@@ -0,0 +1,55 @@
+import React from 'react';
+import TabView from '../components/Tabs/TabView';
+import { shallow, mount } from 'enzyme';
+
+describe('<TabView />', () => {
+  let props;
+  let mountedTab;
+  const tabView = () => {
+    if (!mountedTab) {
+      return mount(<TabView {...props} />);
+    }
+    return mountedTab;
+  };
+  beforeEach(() => {
+    props = {
+      data: undefined
+    };
+    mountedTab = undefined;
+  });
+
+  it('Default active on the first tab and not active on the rest', () => {
+    const tab = tabView().find('.tab');
+    expect(tab.at(0).hasClass('active')).toBe(true);
+    expect(tab.at(1).hasClass('active')).toBe(false);
+    expect(tabView().find('TabGroup').length).toBe(1);
+  });
+
+  it('Default active content is correct', () => {
+    const data = {
+      tabs: ['one', 'two', 'thre'],
+      content: ['1', '2', '3']
+    };
+    props = {
+      data
+    };
+    const tabContent = tabView().find('.tabContent');
+    expect(tabContent.find('div').text()).toBe('1');
+  });
+
+  it('Click on the inactive tab activate the tab', () => {
+    const data = {
+      tabs: ['one', 'two', 'thre'],
+      content: ['1', '2', '3']
+    };
+    props = {
+      data
+    };
+    const tabViewWindow = tabView();
+    const tab1 = tabViewWindow.find('.tab').at(1);
+    const tab1Activated = tab1.simulate('click');
+    expect(tab1Activated.hasClass('active')).toBe(true);
+    const tabContent = tabViewWindow.find('.tabContent');
+    expect(tabContent.render().find('div').text()).toBe('2');
+  });
+});

+ 14 - 0
src/__test__/Toggle.test.js

@@ -0,0 +1,14 @@
+import React from 'react';
+import Toggle from '../components/Toggle';
+import FadeInOut from '../components/Utils/FadeInOut'
+import { mount, shallow } from 'enzyme';
+
+describe('Toggle', () => {
+  it('Clicking the toggle shows the FadeInOut element', () => {
+    const toggoleWindow = mount(<Toggle/>)
+    toggoleWindow.find('button').simulate('click')
+    expect(toggoleWindow.find('FadeInOut')).toBeTruthy()
+    toggoleWindow.find('button').simulate('click')
+    expect(toggoleWindow.contains(<FadeInOut/>)).toBeFalsy()
+  })
+})

+ 1 - 1
src/components/App/App.css

@@ -9,7 +9,7 @@
 
 .App-header {
   background-color: #222;
-  height: 150px;
+  height: 100px;
   padding: 20px;
   color: white;
 }

+ 75 - 0
src/components/Counter/Counter.js

@@ -0,0 +1,75 @@
+import { createStore } from 'redux';
+import { Provider, connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+// action creator
+const ADD = 'ADD';
+const MINUS = 'MINUS';
+function add() {
+  return { type: ADD };
+}
+function minus() {
+  return { type: MINUS };
+}
+
+// reducer
+const app = (state = 0, action) => {
+  switch (action.type) {
+    case ADD:
+      return state + 1;
+    case MINUS:
+      if (state <= 0) {
+        alert('Gotcha. The counter has to be positive!');
+        return state;
+      }
+      return state - 1;
+    default:
+      return state;
+  }
+};
+
+// create store
+const store = createStore(app);
+
+// react component
+const StaticCounter = ({ value, handleClickAdd, handleClickMinus }) =>
+  <div>
+    <h2>{value}</h2>
+    <button onClick={handleClickAdd}>Add </button>
+    <button onClick={handleClickMinus}>Minus </button>
+  </div>;
+
+StaticCounter.propTypes = {
+  value: PropTypes.number.isRequired,
+  handleClickAdd: PropTypes.func.isRequired,
+  handleClickMinus: PropTypes.func.isRequired
+};
+// activate the container StaticCounter
+const mapStateToProps = state => {
+  return {
+    value: state
+  };
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    handleClickAdd: () => {
+      dispatch(add());
+    },
+    handleClickMinus: () => {
+      dispatch(minus());
+    }
+  };
+};
+
+const ActiveCounter = connect(mapStateToProps, mapDispatchToProps)(
+  StaticCounter
+);
+
+// passing the store
+const Counter = () =>
+  <Provider store={store}>
+    <ActiveCounter />
+  </Provider>;
+export default Counter;

+ 19 - 0
src/components/Counter/index.js

@@ -0,0 +1,19 @@
+import React from 'react';
+import Counter from './Counter';
+import SharedTitle from '../Utils/SharedTitle.jsx';
+
+// import './style.css';
+
+const CounterDemo = () =>
+  <div>
+    <SharedTitle />
+    <p>
+      This state of this counter is managed by Redux instead of React. Note that
+      the counter does not change after switching to other components because
+      the state in redux is not managed by the root component.
+    </p>
+    <hr />
+    <Counter />
+  </div>;
+
+export default CounterDemo;

+ 237 - 0
src/components/Form/Form.js

@@ -0,0 +1,237 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { Motion, spring } from 'react-motion';
+
+import './style.css';
+
+export default class Form extends React.Component {
+  static placeHolder = {
+    name: { message: 'Enter your name', color: 'grey', warnings: [] },
+    password: { message: '12345678', color: 'grey', warnings: [] },
+    password2: { message: '12345678', color: 'grey', warnings: [] },
+    comments: { message: 'Enter your comment', color: 'grey', warnings: [] }
+  };
+
+  state = {
+    ...this.constructor.placeHolder
+  };
+
+  validateRequired = name => {
+    const warning = (
+      <Motion defaultStyle={{ x: 0 }} style={{ x: spring(1) }} key="isRequired">
+        {({ x }) =>
+          <span className="warning" style={{ opacity: x }}>
+            This field is required.
+          </span>}
+      </Motion>
+    );
+    const color = this.state[name].color;
+    return this.state[name].message.length > 0 && color !== 'grey'
+      ? null
+      : warning;
+  };
+
+  validateLength = (name, length) => {
+    const warning = (
+      <Motion
+        defaultStyle={{ x: 0 }}
+        style={{ x: spring(1) }}
+        key="isMinLength"
+      >
+        {({ x }) =>
+          <span className="warning" style={{ opacity: x }}>
+            This field should at least have {length} characters.
+          </span>}
+      </Motion>
+    );
+    const color = this.state[name].color;
+    return this.state[name].message.length >= length && color !== 'grey'
+      ? null
+      : warning;
+  };
+
+  validatePasswordMatch = (name1, name2) => {
+    const password1 = this.state[name1].message;
+    const password2 = this.state[name2].message;
+    const warning = (
+      <span className="warning" key="isPwdMatch">
+        Password don't match.
+      </span>
+    );
+    const color1 = this.state[name1].color;
+    return password1 === password2 && color1 !== 'grey' ? null : warning;
+  };
+
+  handleSubmit = event => {
+    // force validate all fields
+    const keys = Object.keys(this.state);
+
+    keys.forEach(name => {
+      const warnings = this.validateItem(name);
+      if (name === 'password' || name === 'password2') {
+        // match two password input behavior
+        this.setState({
+          password: Object.assign({}, this.state.password, {
+            warnings
+          }),
+          password2: Object.assign({}, this.state.password2, {
+            warnings
+          })
+        });
+      } else {
+        this.setState({
+          [name]: Object.assign({}, this.state[name], {
+            warnings
+          })
+        });
+      }
+    });
+
+    const inValidFields = keys.filter(
+      name => this.state[name].warnings.length !== 0
+    );
+    if (inValidFields.length !== 0 || this.state.password.color === 'grey') {
+      event.preventDefault();
+    }
+  };
+
+  handleFocus = event => {
+    const target = event.target;
+    const name = target.name;
+    const color = this.state[name].color;
+    if (color === 'grey') {
+      this.setState({
+        [name]: Object.assign({}, this.state[name], {
+          message: '',
+          color: 'black'
+        })
+      });
+    }
+  };
+
+  wrapWarnings = (...theArgs) => {
+    const validWarnings = theArgs.filter(arg => arg !== null);
+    return validWarnings;
+  };
+
+  validateItem = name => {
+    let warnings;
+    switch (name) {
+      case 'name':
+        warnings = this.wrapWarnings(
+          this.validateLength(name, 5),
+          this.validateRequired(name)
+        );
+        break;
+      case 'password':
+      case 'password2':
+        warnings = this.wrapWarnings(
+          this.validatePasswordMatch('password', 'password2'),
+          this.validateRequired(name)
+        );
+        break;
+      case 'comments':
+        warnings = this.wrapWarnings(
+          this.validateLength(name, 20),
+          this.validateRequired(name)
+        );
+        break;
+      default:
+        break;
+    }
+    return warnings;
+  };
+
+  handleBlur = event => {
+    const target = event.target;
+    const name = target.name;
+    const message = this.constructor.placeHolder[name].message;
+    const warnings = this.validateItem(name);
+    if (target.value.length === 0) {
+      this.setState({
+        [name]: Object.assign({}, this.state[name], {
+          message: message,
+          color: 'grey',
+          warnings
+        })
+      });
+    } else if (name === 'password' || name === 'password2') {
+      // match two password inputs behavior
+      this.setState({
+        password: {
+          ...this.state.password,
+          warnings
+        },
+        password2: {
+          ...this.state.password2,
+          warnings
+        }
+      });
+    } else {
+      this.setState({
+        [name]: {
+          ...this.state[name],
+          warnings
+        }
+      });
+    }
+  };
+
+  handleChange = event => {
+    const target = event.target;
+    const value = target.type === 'checkbox' ? target.checked : target.value;
+    const name = target.name;
+    this.setState({
+      [name]: Object.assign({}, this.state[name], { message: value })
+    });
+  };
+
+  renderInput = (type, name) => {
+    const borderColor = this.state[name].warnings.length ? 'red' : null;
+    const props = {
+      type: type,
+      name: name,
+      id: name,
+      value: this.state[name].message,
+      onFocus: this.handleFocus,
+      onChange: this.handleChange,
+      style: { color: this.state[name].color, borderColor: borderColor },
+      onBlur: this.handleBlur,
+      className: name
+    };
+
+    if (type === 'textarea') {
+      return <textarea {...props} />;
+    }
+    return <input {...props} />;
+  };
+
+  render() {
+    return (
+      <form onSubmit={this.handleSubmit} action={<Link to="/tabss" />}>
+        <label htmlFor="name">
+          Name:
+        </label>
+        {this.renderInput('text', 'name')}
+        {this.state.name.warnings}
+        <label htmlFor="password">
+          Password:
+        </label>
+        {this.renderInput('password', 'password')}
+        {this.state.password.warnings}
+        <label htmlFor="password2">
+          Password:
+        </label>
+        {this.renderInput('password', 'password2')}
+        {this.state.password2.warnings}
+
+        <label htmlFor="comments">
+          Comments:
+        </label>
+        {this.renderInput('textarea', 'comments')}
+        {this.state.comments.warnings}
+        <input type="submit" value="Submit" />
+      </form>
+    );
+  }
+}

+ 29 - 0
src/components/Form/index.js

@@ -0,0 +1,29 @@
+import React from 'react';
+import Form from './Form';
+import SharedTile from '../Utils/SharedTitle';
+
+const FormDemo = () => {
+  return (
+    <div>
+      <SharedTile/>
+      <p>
+        This form is constructed purely in React without any external library.
+        The placeholders and validation are implemented in pure Javascript, thus
+        no HTML5 is required and more importantly, the style of placeholders and
+        warnings are easy to control. However, implementing good validation
+        logic on large forms is still challegning in react as the code could
+        become extremely verbose and repeative. A library is still required to
+        handle more professional and long forms with complex validation process.
+        In addition, using <code>setState</code> multiple times in one function
+        can lead to sublte bugs as this <code>setState</code> is asynchronous.
+        Updating the state with <code>setState</code> then checking certain
+        conditions based on the new state in one function does not work. Use a
+        callback function in <code>setState</code> instead.
+      </p>
+      <hr />
+      <Form />
+    </div>
+  );
+};
+
+export default FormDemo;

+ 46 - 0
src/components/Form/style.css

@@ -0,0 +1,46 @@
+input:not([type=submit]) {
+    width: 150px;
+    margin: 8px 10px;
+    padding: 5px;
+    border: none;
+    border-bottom: 1px solid #ccc;
+    box-sizing: border-box;
+}
+
+input:not([type=submit]):focus {
+    outline: none;
+    border-bottom: 1px solid #66ccff;
+    box-sizing: border-box;
+}
+textarea:focus {
+    outline: none;
+    border-color: #66ccff;
+}
+textarea {
+    width: 200px;
+    height: 100px;
+    margin: 8px 0;
+    border: 1px solid #ccc;
+    border-radius: 4px;
+    box-sizing: border-box;
+    display: block;
+}
+
+label {
+    display: block;
+}
+
+.warning {
+    display: block;
+    width: 400px;
+    color: red;
+    font-size: 80%;
+}
+
+input[type=submit] {
+    margin: 8px 0;
+}
+
+form {
+    width: 500px;
+}

+ 55 - 0
src/components/Modal/Modal.jsx

@@ -0,0 +1,55 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import './style.css';
+
+const ModalWindow = ({ isActive, handleClickClose }) => {
+  if (isActive) {
+    return (
+      <div className={'modal'}>
+        <button className="hide" onClick={handleClickClose}>X</button>
+        <p>
+          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
+          minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+          aliquip ex ea commodo consequat. Duis aute irure dolor in
+          reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+          pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
+          culpa qui officia deserunt mollit anim id est laborum.
+        </p>
+      </div>
+    );
+  }
+  return null;
+};
+
+ModalWindow.PropTypes = {
+  isActive: PropTypes.bool.isRequired,
+  handleClickClose: PropTypes.func.isRequired
+};
+
+export { ModalWindow };
+
+export default class Modal extends React.Component {
+  state = { isActive: false };
+
+  handleClick = () => {
+    this.setState({ isActive: true });
+  };
+
+  handleCloseModal = () => {
+    this.setState({ isActive: false });
+  };
+
+  render() {
+    return (
+      <div>
+        <button className="show" onClick={this.handleClick}>Show Modal</button>
+        <ModalWindow
+          isActive={this.state.isActive}
+          handleClickClose={this.handleCloseModal}
+        />
+      </div>
+    );
+  }
+}

+ 15 - 0
src/components/Modal/index.js

@@ -0,0 +1,15 @@
+import React from 'react';
+import Modal from './Modal';
+import SharedTitle from '../Utils/SharedTitle.jsx';
+
+import './style.css';
+
+const ModalDemo = () =>
+  <div>
+    <SharedTitle />
+    <p>A modal will show after clicking the button</p>
+    <hr />
+    <Modal />
+  </div>;
+
+export default ModalDemo;

+ 31 - 0
src/components/Modal/style.css

@@ -0,0 +1,31 @@
+.modal {
+	display: block;
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	z-index: 10;
+	background-color: rgba(0,0,0,0.5);
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+
+.modal p {
+	width: 90vw;
+	background-color: #fff;
+	padding: 10px;
+}
+
+@media screen and (min-width: 600px) {
+	.modal p {
+		width: 40vw;
+	}
+	
+}
+button.hide {
+	position: fixed;
+	right: 20px;
+	top: 20px;
+}

+ 7 - 5
src/components/Nav/NavLink.js

@@ -1,9 +1,11 @@
 // modules/NavLink.js
-import React from 'react'
-import { NavLink } from 'react-router-dom'
+import React from 'react';
+import { NavLink } from 'react-router-dom';
 
-export default React.createClass({
+export default class MyNav extends React.Component {
   render() {
-    return <NavLink {...this.props} activeStyle={{backgroundColor: "#111"}} />
+    return (
+      <NavLink {...this.props} activeStyle={{ backgroundColor: '#111' }} />
+    );
   }
-})
+}

+ 14 - 13
src/components/Nav/index.js

@@ -1,17 +1,18 @@
 import React from 'react';
 import NavLink from './NavLink';
 
-class Nav extends React.Component {
-  	render() {
-  		return (
-		<ul className="nav">
-		<li><NavLink to="/" exact={true}>Home</NavLink></li>
-		<li><NavLink to="/Toggle" >Toggle</NavLink></li>
-		<li><NavLink to="/tabs" >Tab View</NavLink></li>
-		<li><NavLink to="/tabss" >Test 404</NavLink></li>
-		</ul>
-  		)
-	}
-}
+import './style.css';
 
-export default Nav;
+const Nav = () =>
+  <ul className="nav">
+    <li><NavLink to="/" exact={true}>Home</NavLink></li>
+    <li><NavLink to="/toggle">Toggle</NavLink></li>
+    <li><NavLink to="/tabs">Tab View</NavLink></li>
+    <li><NavLink to="/counter">Counter</NavLink></li>
+    <li><NavLink to="/modal">Modal</NavLink></li>
+    <li><NavLink to="/slideshow">Slide</NavLink></li>
+    <li><NavLink to="/form">Form</NavLink></li>
+    <li><NavLink to="/tabss">Test 404</NavLink></li>
+  </ul>;
+
+export default Nav;

+ 35 - 0
src/components/Nav/style.css

@@ -0,0 +1,35 @@
+.nav {
+    list-style-type: none;
+    margin: 0;
+    padding: 0;
+    overflow: hidden;
+    background-color: #333;
+    height: auto;
+}
+
+.nav li {
+    width: 100px;
+}
+
+.nav li a {
+    display: block;
+    color: white;
+    text-align: center;
+    padding: 14px 16px;
+    text-decoration: none;
+}
+
+/* Change the link color to #111 (black) on hover */
+.nav li a:hover {
+    background-color: #111;
+}
+
+@media  (max-width: 800px) {
+    .nav ul { 
+        height: auto;
+        width: 100vw;
+    }
+    .nav li {
+        float: left;
+    }
+}

+ 88 - 0
src/components/SlideShow/SlideShow.js

@@ -0,0 +1,88 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import FlipMove from 'react-flip-move';
+
+class Slide extends React.Component {
+  static propTypes = {
+    index: PropTypes.number.isRequired,
+    center: PropTypes.string.isRequired,
+    zIndex: PropTypes.number
+  };
+  render() {
+    const className = 'slide slide' + this.props.index + this.props.center;
+    return (
+      <div className={className} style={{ zIndex: this.props.zIndex }}>
+        <h2>{this.props.slide}</h2>
+      </div>
+    );
+  }
+}
+
+export default class SlideShow extends React.Component {
+  state = {
+    indices: [0, 1, 2, 3, 4],
+    direction: null
+  };
+
+  renderSlides = () => {
+    let nSlide = this.state.indices.length;
+    return (
+      <div className="slide-group">
+        <FlipMove duration={600} easing="ease-in-out">
+          {this.state.indices.map((slide, index) => {
+            const center = index === (nSlide - 1) / 2 ? ' center' : '';
+            let zIndex = null;
+            if (
+              this.state.direction === 'left' &&
+              index === this.state.indices.length - 1
+            ) {
+              zIndex = -10;
+            }
+            if (this.state.direction === 'right' && index === 0) {
+              zIndex = -10;
+            }
+            return (
+              <Slide
+                index={index}
+                center={center}
+                slide={slide}
+                key={slide}
+                zIndex={zIndex}
+              />
+            );
+          })}
+        </FlipMove>
+      </div>
+    );
+  };
+  handleLeftClick = () => {
+    let shiftedState = this.state.indices.slice(1);
+    let newState = [...shiftedState, this.state.indices[0]];
+    this.setState({ indices: newState });
+    this.setState({ direction: 'left' });
+  };
+
+  handleRightClick = () => {
+    let shiftedState = this.state.indices.slice(0, -1);
+    let newState = [
+      this.state.indices[this.state.indices.length - 1],
+      ...shiftedState
+    ];
+    this.setState({ indices: newState });
+    this.setState({ direction: 'right' });
+  };
+
+  render() {
+    return (
+      <div className="slideshow">
+        <button className="left" onClick={this.handleLeftClick}>
+          &lt;
+        </button>
+        {this.renderSlides()}
+        <button className="right" onClick={this.handleRightClick}>
+          &gt;
+        </button>
+      </div>
+    );
+  }
+}

+ 19 - 0
src/components/SlideShow/index.js

@@ -0,0 +1,19 @@
+import React from 'react';
+import SlideShow from './SlideShow';
+import SharedTitle from '../Utils/SharedTitle.jsx';
+
+import './style.css';
+
+const ModalDemo = () =>
+  <div>
+    <SharedTitle />
+    <p>
+      Clicking the button will slide the blocks and the center block is always
+      colored. The animation is borrowed from React-flip-move library with some
+      hack. Overall doing transition animation in React is a bit hard.{' '}
+    </p>
+    <hr />
+    <SlideShow />
+  </div>;
+
+export default ModalDemo;

+ 26 - 0
src/components/SlideShow/style.css

@@ -0,0 +1,26 @@
+.slide0, .slide4, .slide1, .slide2, .slide3 {
+	width: 10vw;
+	height: 100px;
+	border: 2px solid black;
+	text-align: center;
+	float: left;
+	background-color: #fff;
+}
+
+.slide4 {
+	position: relative;
+	z-index: -1;
+}
+
+.slide0 {
+	position: relative;
+	z-index: -1;
+}
+.slide.center {
+	background-color: #666; 
+    transition: background-color 1000ms linear;
+}
+button.left, button.right {
+	height: 104px;
+	float: left;
+}

File diff suppressed because it is too large
+ 66 - 0
src/components/Tabs/TabView.jsx


+ 16 - 6
src/components/Tabs/index.js

@@ -1,9 +1,19 @@
 import React from 'react';
+import TabView from './TabView';
+import SharedTitle from '../Utils/SharedTitle.jsx';
 
-function Tabs(props) {
-	return (
-		<h2>The navigation bar is an example of tab view.</h2>
-	)
-}
+import './style.css';
 
-export default Tabs
+const TabViewDemo = () =>
+  <div>
+    <SharedTitle />
+    <p>
+      The navigation bar is itself a tab view, althoug it is constructed
+      with react-route to update URL. The following is a pure tab view that
+      does not interfere with URL.
+    </p>
+    <hr />
+    <TabView />
+  </div>;
+
+export default TabViewDemo;

+ 25 - 0
src/components/Tabs/style.css

@@ -0,0 +1,25 @@
+
+.tab {
+	border: 0;
+	border-top: 1px solid black;
+	border-left: 1px solid black;
+	border-right: 1px solid black;
+	border-radius: 5px 5px 0 0;
+	background-color: grey;
+	padding: 10px;
+	margin-right: 5px;
+	color: white;
+	display: inline-block;
+	outline:none;
+	width: 80px;
+}
+
+.tab.active {
+	background-color: #111;
+	box-shadow: 1px 2px 5px grey;
+}
+
+.tabContent {
+	padding: 10px 10px;
+	border: 1px solid black;
+}

+ 39 - 0
src/components/Toggle/Toggle.jsx

@@ -0,0 +1,39 @@
+import React from 'react';
+import { Motion, spring } from 'react-motion';
+import FadeInOut from '../Utils/FadeInOut'
+
+
+export default class Toggle extends React.Component {
+  state = {
+    open: false,
+  };
+
+  handleClick = () => {
+    this.setState({ open: !this.state.open });
+  };
+
+  render() {
+    const animatedContent = this.state.open ? [{key: 'FadeInOut',data:'FadeInOut', opacity: spring(1)}] : null
+    return (
+      <div>
+        <button onClick={this.handleClick}>
+          Toggle
+        </button>
+        <h3>Status: {this.state.open ? 'open' : 'closed'}</h3>
+        <Motion style={{ x: spring(this.state.open ? 200 : 0) }}>
+          {({ x }) =>
+            <div className="toggle-bg">
+              <div
+                className="toggle-block"
+                onClick={this.handleClick}
+                style={{
+                  transform: `translate3d(${x}px, 0, 0)`,
+                }}
+              />
+            </div>}
+        </Motion>
+        <FadeInOut content={animatedContent}/>
+      </div>
+    );
+  }
+}

+ 15 - 33
src/components/Toggle/index.js

@@ -1,37 +1,19 @@
-import React from 'react';
-import {Motion, spring} from 'react-motion';
+import React from 'react'
+import Toggle from './Toggle'
+import SharedTitle from '../Utils/SharedTitle'
 
 import './style.css'
 
+const ToggleDemo = () => (
+  <div>
+    <SharedTitle/>
+     <p>
+          Try the toggle which controls the status and animation. The block
+          itself is also clickable. The animation is created using React-Motion.
+     </p>
+         <hr />
+     <Toggle/>
+  </div>
+)
 
-export default class Toggle extends React.Component {
-	state = {
-		open:false,
-	}
-
-	handleClick = () => {
-		this.setState({open: !this.state.open});
-	}
-
-	render() {
-		return (
-			<div>
-				<h1>Try the toggole which controls the status and animation </h1>
-				<button onClick={this.handleClick}>
-				Toggle
-				</button>
-				<h2>Open: {this.state.open.toString()}</h2>
-				<Motion style={{x: spring(this.state.open ? 200 : 0)}}>
-				{({x}) => 
-					<div className="toggle-bg">
-					<div className="toggle-block" style={{
-						transform: `translate3d(${x}px, 0, 0)`,
-					}}>
-					</div>
-					</div>
-				}
-				</Motion>
-			</div>
-		)
-	}
-}
+export default ToggleDemo

+ 29 - 0
src/components/Utils/FadeInOut.js

@@ -0,0 +1,29 @@
+import React from 'react'
+import {TransitionMotion, spring} from 'react-motion'
+
+export default class FadeInOut extends React.Component {
+  willEnter = () => ({opacity: 0.01})
+  willLeave = () => ({opacity: spring(0)})
+  render() {
+    return (
+      <TransitionMotion
+        styles={this.props.content ? this.props.content.map(({key    ,data,...rest}) => ({
+      key: key,
+          data: data,
+      style: rest
+    })) : []}
+    willEnter={this.willEnter}
+    willLeave={this.willLeave}
+    >
+        {(styles) => {
+          return (
+      <div>
+        { styles.map(({ key, data, style}) => (
+          <div key={key} style={{...style}}>{ data }</div>
+        ))}
+            </div>)
+        }}
+  </TransitionMotion>
+    )
+  }
+}

+ 5 - 0
src/components/Utils/SharedTitle.jsx

@@ -0,0 +1,5 @@
+import React from 'react';
+
+const SharedTile = () => <h2>Comments</h2>
+
+export default SharedTile

+ 34 - 19
src/index.css

@@ -1,31 +1,46 @@
+.container {
+	display: -webkit-box;
+  display: -moz-box;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  display: flex;  
+  -webkit-flex-flow: row wrap;
+  flex-flow: row wrap;
+  height: 100%;
+}
+
+.App {
+	flex: 0 1 100%;
+}
+
+.nav {
+	bottom: 0;
+}
+
+
 body {
   margin: 0;
   padding: 0;
   font-family: sans-serif;
 }
 
-ul {
-    list-style-type: none;
-    margin: 0;
-    padding: 0;
-    overflow: hidden;
-    background-color: #333;
+main {
+	padding: 20px;
 }
 
-li {
-    float: left;
-    width: 100px;
+.components {
+    width: 100vw;
 }
 
-li a {
-    display: block;
-    color: white;
-    text-align: center;
-    padding: 14px 16px;
-    text-decoration: none;
+@media screen and (min-width: 800px) {
+  .components {
+    width: 70vw;
+  }
 }
 
-/* Change the link color to #111 (black) on hover */
-li a:hover {
-    background-color: #111;
-}
+code {
+  font-family: Courier, monospace;
+  background-color: rgba(27, 31, 35, 0.05);
+  border-radius: 3px;
+  font-size: 85%;
+  padding: 0.2em; }

+ 25 - 20
src/routes.js

@@ -1,31 +1,36 @@
 // src/routes.js
 import React from 'react';
-import {
-  BrowserRouter as Router,
-  Route,
-  Switch
-} from 'react-router-dom'
+import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
 
 import App from './components/App';
 import Nav from './components/Nav';
-import Toggle from './components/Toggle';
 import Home from './components/Home';
-import Tabs from './components/Tabs';
+import ToggleDemo from './components/Toggle';
+import TabViewDemo from './components/Tabs';
+import ModalDemo from './components/Modal';
+import SlideShowDemo from './components/SlideShow';
+import CounterDemo from './components/Counter';
+import FormDemo from './components/Form';
 import NotFound from './components/NotFound';
 
-const Routes = (props) => (
+const Routes = props =>
   <Router {...props}>
-  	<div>
-  	<App />
-    <Nav />
-  	<Switch>
-  	<Route path="/" exact component={Home} />
-    <Route path="/Toggle" component={Toggle} />
-    <Route path="/tabs" component={Tabs} />
-    <Route component={NotFound} />
-    </Switch>
+    <div className="container">
+      <App />
+      <Nav />
+      <main className="components">
+        <Switch>
+          <Route path="/" exact component={Home} />
+          <Route path="/toggle" component={ToggleDemo} />
+          <Route path="/tabs" component={TabViewDemo} />
+          <Route path="/counter" component={CounterDemo} />
+          <Route path="/modal" component={ModalDemo} />
+          <Route path="/slideshow" component={SlideShowDemo} />
+          <Route path="/form" component={FormDemo} />
+          <Route component={NotFound} />
+        </Switch>
+      </main>
     </div>
-  </Router>
-);
+  </Router>;
 
-export default Routes;
+export default Routes;