Remove boilerplate add search form
This commit is contained in:
Родитель
1cc9c60fda
Коммит
3f12efa66b
4
app.json
4
app.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-redux-universal-hot-example",
|
||||
"description": "Example of an isomorphic (universal) webapp using react redux and hot reloading",
|
||||
"name": "mozlando-frontend-demo",
|
||||
"description": "Example of addons frontend using react, redux and universal server-rendering.",
|
||||
"repository": "https://github.com/erikras/react-redux-universal-hot-example",
|
||||
"logo": "http://node-js-sample.herokuapp.com/node.svg",
|
||||
"keywords": [
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import React, {Component, PropTypes} from 'react';
|
||||
import {connectMultireducer} from 'multireducer';
|
||||
import {increment} from 'redux/modules/counter';
|
||||
|
||||
@connectMultireducer(
|
||||
state => ({count: state.count}),
|
||||
{increment})
|
||||
export default class CounterButton extends Component {
|
||||
static propTypes = {
|
||||
count: PropTypes.number,
|
||||
increment: PropTypes.func.isRequired,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
props = {
|
||||
className: ''
|
||||
}
|
||||
|
||||
render() {
|
||||
const {count, increment} = this.props; // eslint-disable-line no-shadow
|
||||
let {className} = this.props;
|
||||
className += ' btn btn-default';
|
||||
return (
|
||||
<button className={className} onClick={increment}>
|
||||
You have clicked me {count} time{count === 1 ? '' : 's'}.
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const GithubButton = (props) => {
|
||||
const {user, repo, type, width, height, count, large} = props;
|
||||
let src = `https://ghbtns.com/github-btn.html?user=${user}&repo=${repo}&type=${type}`;
|
||||
if (count) src += '&count=true';
|
||||
if (large) src += '&size=large';
|
||||
|
||||
return (
|
||||
<iframe
|
||||
src={src}
|
||||
frameBorder="0"
|
||||
allowTransparency="true"
|
||||
scrolling="0"
|
||||
width={width}
|
||||
height={height}
|
||||
style={{border: 'none', width: width, height: height}}></iframe>
|
||||
);
|
||||
};
|
||||
|
||||
GithubButton.propTypes = {
|
||||
user: React.PropTypes.string.isRequired,
|
||||
repo: React.PropTypes.string.isRequired,
|
||||
type: React.PropTypes.oneOf(['star', 'watch', 'fork', 'follow']).isRequired,
|
||||
width: React.PropTypes.number.isRequired,
|
||||
height: React.PropTypes.number.isRequired,
|
||||
count: React.PropTypes.bool,
|
||||
large: React.PropTypes.bool
|
||||
};
|
||||
|
||||
export default GithubButton;
|
|
@ -0,0 +1,30 @@
|
|||
import React, {Component, PropTypes} from 'react';
|
||||
import {reduxForm} from 'redux-form';
|
||||
|
||||
@reduxForm({
|
||||
form: 'search',
|
||||
fields: ['q'],
|
||||
})
|
||||
export default class SearchForm extends Component {
|
||||
|
||||
static propTypes = {
|
||||
q: PropTypes.string,
|
||||
fields: PropTypes.object.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {fields: {q}, handleSubmit} = this.props;
|
||||
const styles = require('./SearchForm.scss');
|
||||
|
||||
return (
|
||||
<form className={styles.searchform + ' form-inline'} action="/search" method="GET" onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="search" className="sr-only">Search</label>
|
||||
<input className="form-control" id="search" type="search" placeholder="e.g: privacy" {...q} />
|
||||
<button className="btn btn-primary" type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.searchform {
|
||||
padding: 2em 0;
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
import React, {Component, PropTypes} from 'react';
|
||||
import {reduxForm} from 'redux-form';
|
||||
import surveyValidation from './surveyValidation';
|
||||
|
||||
function asyncValidate(data) {
|
||||
// TODO: figure out a way to move this to the server. need an instance of ApiClient
|
||||
if (!data.email) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
const errors = {};
|
||||
let valid = true;
|
||||
if (~['bobby@gmail.com', 'timmy@microsoft.com'].indexOf(data.email)) {
|
||||
errors.email = 'Email address already used';
|
||||
valid = false;
|
||||
}
|
||||
if (valid) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(errors);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
@reduxForm({
|
||||
form: 'survey',
|
||||
fields: ['name', 'email', 'occupation', 'currentlyEmployed', 'sex'],
|
||||
validate: surveyValidation,
|
||||
asyncValidate,
|
||||
asyncBlurFields: ['email']
|
||||
})
|
||||
export default
|
||||
class SurveyForm extends Component {
|
||||
static propTypes = {
|
||||
active: PropTypes.string,
|
||||
asyncValidating: PropTypes.bool.isRequired,
|
||||
fields: PropTypes.object.isRequired,
|
||||
dirty: PropTypes.bool.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
resetForm: PropTypes.func.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
valid: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
asyncValidating,
|
||||
dirty,
|
||||
fields: {name, email, occupation, currentlyEmployed, sex},
|
||||
active,
|
||||
handleSubmit,
|
||||
invalid,
|
||||
resetForm,
|
||||
pristine,
|
||||
valid
|
||||
} = this.props;
|
||||
const styles = require('./SurveyForm.scss');
|
||||
const renderInput = (field, label, showAsyncValidating) =>
|
||||
<div className={'form-group' + (field.error && field.touched ? ' has-error' : '')}>
|
||||
<label htmlFor={field.name} className="col-sm-2">{label}</label>
|
||||
<div className={'col-sm-8 ' + styles.inputGroup}>
|
||||
{showAsyncValidating && asyncValidating && <i className={'fa fa-cog fa-spin ' + styles.cog}/>}
|
||||
<input type="text" className="form-control" id={field.name} {...field}/>
|
||||
{field.error && field.touched && <div className="text-danger">{field.error}</div>}
|
||||
<div className={styles.flags}>
|
||||
{field.dirty && <span className={styles.dirty} title="Dirty">D</span>}
|
||||
{field.active && <span className={styles.active} title="Active">A</span>}
|
||||
{field.visited && <span className={styles.visited} title="Visited">V</span>}
|
||||
{field.touched && <span className={styles.touched} title="Touched">T</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form className="form-horizontal" onSubmit={handleSubmit}>
|
||||
{renderInput(name, 'Full Name')}
|
||||
{renderInput(email, 'Email', true)}
|
||||
{renderInput(occupation, 'Occupation')}
|
||||
<div className="form-group">
|
||||
<label htmlFor="currentlyEmployed" className="col-sm-2">Currently Employed?</label>
|
||||
<div className="col-sm-8">
|
||||
<input type="checkbox" id="currentlyEmployed" {...currentlyEmployed}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="col-sm-2">Sex</label>
|
||||
<div className="col-sm-8">
|
||||
<input type="radio" id="sex-male" {...sex} value="male" checked={sex.value === 'male'}/>
|
||||
<label htmlFor="sex-male" className={styles.radioLabel}>Male</label>
|
||||
<input type="radio" id="sex-female" {...sex} value="female" checked={sex.value === 'female'}/>
|
||||
<label htmlFor="sex-female" className={styles.radioLabel}>Female</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div className="col-sm-offset-2 col-sm-10">
|
||||
<button className="btn btn-success" onClick={handleSubmit}>
|
||||
<i className="fa fa-paper-plane"/> Submit
|
||||
</button>
|
||||
<button className="btn btn-warning" onClick={resetForm} style={{marginLeft: 15}}>
|
||||
<i className="fa fa-undo"/> Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h4>Props from redux-form</h4>
|
||||
|
||||
<table className="table table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Active Field</th>
|
||||
<td>{active}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Dirty</th>
|
||||
<td className={dirty ? 'success' : 'danger'}>{dirty ? 'true' : 'false'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Pristine</th>
|
||||
<td className={pristine ? 'success' : 'danger'}>{pristine ? 'true' : 'false'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Valid</th>
|
||||
<td className={valid ? 'success' : 'danger'}>{valid ? 'true' : 'false'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Invalid</th>
|
||||
<td className={invalid ? 'success' : 'danger'}>{invalid ? 'true' : 'false'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
.inputGroup {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flags {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 7px;
|
||||
& > * {
|
||||
margin: 0 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4);
|
||||
color: white;
|
||||
float: right;
|
||||
text-align: center;
|
||||
}
|
||||
.active {
|
||||
background: linear-gradient(#cc0, #aa0);
|
||||
color: black;
|
||||
}
|
||||
.dirty {
|
||||
background: linear-gradient(#090, #060);
|
||||
}
|
||||
.visited {
|
||||
background: linear-gradient(#009, #006);
|
||||
}
|
||||
.touched {
|
||||
background: linear-gradient(#099, #066);
|
||||
}
|
||||
}
|
||||
|
||||
.radioLabel {
|
||||
margin: 0 25px 0 5px;
|
||||
}
|
||||
.cog {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 10px;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import memoize from 'lru-memoize';
|
||||
import {createValidator, required, maxLength, email} from 'utils/validation';
|
||||
|
||||
const surveyValidation = createValidator({
|
||||
name: [required, maxLength(10)],
|
||||
email: [required, email],
|
||||
occupation: maxLength(20) // single rules don't have to be in an array
|
||||
});
|
||||
export default memoize(10)(surveyValidation);
|
|
@ -1,76 +0,0 @@
|
|||
import React, {Component, PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {reduxForm} from 'redux-form';
|
||||
import widgetValidation, {colors} from './widgetValidation';
|
||||
import * as widgetActions from 'redux/modules/widgets';
|
||||
|
||||
@connect(
|
||||
state => ({
|
||||
saveError: state.widgets.saveError
|
||||
}),
|
||||
dispatch => bindActionCreators(widgetActions, dispatch)
|
||||
)
|
||||
@reduxForm({
|
||||
form: 'widget',
|
||||
fields: ['id', 'color', 'sprocketCount', 'owner'],
|
||||
validate: widgetValidation
|
||||
})
|
||||
export default class WidgetForm extends Component {
|
||||
static propTypes = {
|
||||
fields: PropTypes.object.isRequired,
|
||||
editStop: PropTypes.func.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
save: PropTypes.func.isRequired,
|
||||
submitting: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
formKey: PropTypes.string.isRequired,
|
||||
values: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { editStop, fields: {id, color, sprocketCount, owner}, formKey, handleSubmit, invalid,
|
||||
pristine, save, submitting, saveError: { [formKey]: saveError }, values } = this.props;
|
||||
const styles = require('containers/Widgets/Widgets.scss');
|
||||
return (
|
||||
<tr className={submitting ? styles.saving : ''}>
|
||||
<td className={styles.idCol}>{id.value}</td>
|
||||
<td className={styles.colorCol}>
|
||||
<select name="color" className="form-control" {...color}>
|
||||
{colors.map(valueColor => <option value={valueColor} key={valueColor}>{valueColor}</option>)}
|
||||
</select>
|
||||
{color.error && color.touched && <div className="text-danger">{color.error}</div>}
|
||||
</td>
|
||||
<td className={styles.sprocketsCol}>
|
||||
<input type="text" className="form-control" {...sprocketCount}/>
|
||||
{sprocketCount.error && sprocketCount.touched && <div className="text-danger">{sprocketCount.error}</div>}
|
||||
</td>
|
||||
<td className={styles.ownerCol}>
|
||||
<input type="text" className="form-control" {...owner}/>
|
||||
{owner.error && owner.touched && <div className="text-danger">{owner.error}</div>}
|
||||
</td>
|
||||
<td className={styles.buttonCol}>
|
||||
<button className="btn btn-default"
|
||||
onClick={() => editStop(formKey)}
|
||||
disabled={submitting}>
|
||||
<i className="fa fa-ban"/> Cancel
|
||||
</button>
|
||||
<button className="btn btn-success"
|
||||
onClick={handleSubmit(() => save(values)
|
||||
.then(result => {
|
||||
if (result && typeof result.error === 'object') {
|
||||
return Promise.reject(result.error);
|
||||
}
|
||||
})
|
||||
)}
|
||||
disabled={pristine || invalid || submitting}>
|
||||
<i className={'fa ' + (submitting ? 'fa-cog fa-spin' : 'fa-cloud')}/> Save
|
||||
</button>
|
||||
{saveError && <div className="text-danger">{saveError}</div>}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import {createValidator, required, maxLength, integer, oneOf} from 'utils/validation';
|
||||
|
||||
export const colors = ['Blue', 'Fuchsia', 'Green', 'Orange', 'Red', 'Taupe'];
|
||||
|
||||
const widgetValidation = createValidator({
|
||||
color: [required, oneOf(colors)],
|
||||
sprocketCount: [required, integer],
|
||||
owner: [required, maxLength(30)]
|
||||
});
|
||||
export default widgetValidation;
|
|
@ -5,9 +5,6 @@
|
|||
*
|
||||
*/
|
||||
|
||||
export CounterButton from './CounterButton/CounterButton';
|
||||
export GithubButton from './GithubButton/GithubButton';
|
||||
export InfoBar from './InfoBar/InfoBar';
|
||||
export MiniInfoBar from './MiniInfoBar/MiniInfoBar';
|
||||
export SurveyForm from './SurveyForm/SurveyForm';
|
||||
export WidgetForm from './WidgetForm/WidgetForm';
|
||||
export SearchForm from './SearchForm/SearchForm';
|
||||
|
|
|
@ -15,8 +15,8 @@ module.exports = Object.assign({
|
|||
apiHost: process.env.APIHOST || 'localhost',
|
||||
apiPort: process.env.APIPORT,
|
||||
app: {
|
||||
title: 'React Redux Example',
|
||||
description: 'All the modern best practices in one example.',
|
||||
title: 'Mozlando Frontend demo',
|
||||
description: 'Example of addons frontend using react, redux and universal server-rendering.',
|
||||
meta: {
|
||||
charSet: 'utf-8',
|
||||
property: {
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import React, {Component} from 'react';
|
||||
import DocumentMeta from 'react-document-meta';
|
||||
import { MiniInfoBar } from 'components';
|
||||
import config from '../../config';
|
||||
|
||||
export default class About extends Component {
|
||||
|
||||
state = {
|
||||
showKitten: false
|
||||
}
|
||||
|
||||
handleToggleKitten = () => this.setState({showKitten: !this.state.showKitten});
|
||||
|
||||
render() {
|
||||
const {showKitten} = this.state;
|
||||
const kitten = require('./kitten.jpg');
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>About Us</h1>
|
||||
<DocumentMeta title={config.app.title + ': About Us'}/>
|
||||
|
||||
<p>This project was orginally created by Erik Rasmussen
|
||||
(<a href="https://twitter.com/erikras" target="_blank">@erikras</a>), but has since seen many contributions
|
||||
from the open source community. Thank you to <a
|
||||
href="https://github.com/erikras/react-redux-universal-hot-example/graphs/contributors"
|
||||
target="_blank">all the contributors</a>.
|
||||
</p>
|
||||
|
||||
<h3>Mini Bar <span style={{color: '#aaa'}}>(not that kind)</span></h3>
|
||||
|
||||
<p>Hey! You found the mini info bar! The following component is display-only. Note that it shows the same
|
||||
time as the info bar.</p>
|
||||
|
||||
<MiniInfoBar/>
|
||||
|
||||
<h3>Images</h3>
|
||||
|
||||
<p>
|
||||
Psst! Would you like to see a kitten?
|
||||
|
||||
<button className={'btn btn-' + (showKitten ? 'danger' : 'success')}
|
||||
style={{marginLeft: 50}}
|
||||
onClick={this.handleToggleKitten}>
|
||||
{showKitten ? 'No! Take it away!' : 'Yes! Please!'}</button>
|
||||
</p>
|
||||
|
||||
{showKitten && <div><img src={kitten}/></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
Двоичные данные
src/containers/About/kitten.jpg
Двоичные данные
src/containers/About/kitten.jpg
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 90 KiB |
|
@ -61,7 +61,7 @@ export default class App extends Component {
|
|||
<DocumentMeta {...config.app}/>
|
||||
<Navbar fixedTop toggleNavKey={0}>
|
||||
<NavBrand>
|
||||
<IndexLink to="/" activeStyle={{color: '#33e0ff'}}>
|
||||
<IndexLink to="/" className={styles.navbar} activeStyle={{color: '#666'}}>
|
||||
<div className={styles.brand}/>
|
||||
<span>{config.app.title}</span>
|
||||
</IndexLink>
|
||||
|
@ -69,20 +69,6 @@ export default class App extends Component {
|
|||
|
||||
<CollapsibleNav eventKey={0}>
|
||||
<Nav navbar>
|
||||
{user && <LinkContainer to="/chat">
|
||||
<NavItem eventKey={1}>Chat</NavItem>
|
||||
</LinkContainer>}
|
||||
|
||||
<LinkContainer to="/widgets">
|
||||
<NavItem eventKey={2}>Widgets</NavItem>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/survey">
|
||||
<NavItem eventKey={3}>Survey</NavItem>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/about">
|
||||
<NavItem eventKey={4}>About Us</NavItem>
|
||||
</LinkContainer>
|
||||
|
||||
{!user &&
|
||||
<LinkContainer to="/login">
|
||||
<NavItem eventKey={5}>Login</NavItem>
|
||||
|
@ -97,7 +83,7 @@ export default class App extends Component {
|
|||
{user &&
|
||||
<p className={styles.loggedInMessage + ' navbar-text'}>Logged in as <strong>{user.name}</strong>.</p>}
|
||||
<Nav navbar right>
|
||||
<NavItem eventKey={1} target="_blank" title="View on Github" href="https://github.com/erikras/react-redux-universal-hot-example">
|
||||
<NavItem eventKey={1} target="_blank" title="View on Github" href="https://github.com/mozilla/mozlando-frontend-demo">
|
||||
<i className="fa fa-github"/>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
|
@ -108,13 +94,6 @@ export default class App extends Component {
|
|||
{this.props.children}
|
||||
</div>
|
||||
<InfoBar/>
|
||||
|
||||
<div className="well text-center">
|
||||
Have questions? Ask for help <a
|
||||
href="https://github.com/erikras/react-redux-universal-hot-example/issues"
|
||||
target="_blank">on Github</a> or in the <a
|
||||
href="https://discord.gg/0ZcbPKXt5bZZb1Ko" target="_blank">#react-redux-universal</a> Discord channel.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,4 @@
|
|||
.app {
|
||||
.brand {
|
||||
position: absolute;
|
||||
$size: 40px;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
display: inline-block;
|
||||
background: #2d2d2d url('../Home/logo.png') no-repeat center center;
|
||||
width: $size;
|
||||
height: $size;
|
||||
background-size: 80%;
|
||||
margin: 0 10px 0 0;
|
||||
border-radius: $size / 2;
|
||||
}
|
||||
nav :global(.fa) {
|
||||
font-size: 2em;
|
||||
line-height: 20px;
|
||||
|
@ -20,3 +7,7 @@
|
|||
.appContent {
|
||||
margin: 50px 0; // for fixed navbar
|
||||
}
|
||||
.navbar {
|
||||
margin: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
import React, {Component, PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
@connect(
|
||||
state => ({user: state.auth.user})
|
||||
)
|
||||
export default class Chat extends Component {
|
||||
|
||||
static propTypes = {
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
state = {
|
||||
message: '',
|
||||
messages: []
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (socket && !this.onMsgListener) {
|
||||
this.onMsgListener = socket.on('msg', this.onMessageReceived);
|
||||
|
||||
setTimeout(() => {
|
||||
socket.emit('history', {offset: 0, length: 100});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (socket && this.onMsgListener) {
|
||||
socket.removeListener('on', this.onMsgListener);
|
||||
this.onMsgListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMessageReceived = (data) => {
|
||||
const messages = this.state.messages;
|
||||
messages.push(data);
|
||||
this.setState({messages});
|
||||
}
|
||||
|
||||
handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const msg = this.state.message;
|
||||
|
||||
this.setState({message: ''});
|
||||
|
||||
socket.emit('msg', {
|
||||
from: this.props.user.name,
|
||||
text: msg
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = require('./Chat.scss');
|
||||
const {user} = this.props;
|
||||
|
||||
return (
|
||||
<div className={style.chat + ' container'}>
|
||||
<h1 className={style}>Chat</h1>
|
||||
|
||||
{user &&
|
||||
<div>
|
||||
<ul>
|
||||
{this.state.messages.map((msg) => {
|
||||
return <li key={`chat.msg.${msg.id}`}>{msg.from}: {msg.text}</li>;
|
||||
})}
|
||||
</ul>
|
||||
<form className="login-form" onSubmit={this.handleSubmit}>
|
||||
<input type="text" ref="message" placeholder="Enter your message"
|
||||
value={this.state.message}
|
||||
onChange={(event) => {
|
||||
this.setState({message: event.target.value});
|
||||
}
|
||||
}/>
|
||||
<button className="btn" onClick={this.handleSubmit}>Send</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
.chat {
|
||||
input {
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
form {
|
||||
margin: 30px 0;
|
||||
:global(.btn) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,171 +1,27 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { CounterButton, GithubButton } from 'components';
|
||||
import { SearchForm } from '../../components';
|
||||
import config from '../../config';
|
||||
|
||||
|
||||
export default class Home extends Component {
|
||||
|
||||
handleSubmit(event) {
|
||||
window.alert('Data submitted! ' + JSON.stringify(event));
|
||||
}
|
||||
|
||||
render() {
|
||||
const styles = require('./Home.scss');
|
||||
// require the logo image both from client and server
|
||||
const logoImage = require('./logo.png');
|
||||
|
||||
return (
|
||||
<div className={styles.home}>
|
||||
<div className={styles.masthead}>
|
||||
<div className="container">
|
||||
<div className={styles.logo}>
|
||||
<p>
|
||||
<img src={logoImage}/>
|
||||
</p>
|
||||
</div>
|
||||
<h1>{config.app.title}</h1>
|
||||
|
||||
<h2>{config.app.description}</h2>
|
||||
|
||||
<p>
|
||||
<a className={styles.github} href="https://github.com/erikras/react-redux-universal-hot-example"
|
||||
target="_blank">
|
||||
<i className="fa fa-github"/> View on Github
|
||||
</a>
|
||||
</p>
|
||||
<GithubButton user="erikras"
|
||||
repo="react-redux-universal-hot-example"
|
||||
type="star"
|
||||
width={160}
|
||||
height={30}
|
||||
count large/>
|
||||
<GithubButton user="erikras"
|
||||
repo="react-redux-universal-hot-example"
|
||||
type="fork"
|
||||
width={160}
|
||||
height={30}
|
||||
count large/>
|
||||
|
||||
<p className={styles.humility}>
|
||||
Created and maintained by <a href="https://twitter.com/erikras" target="_blank">@erikras</a>.
|
||||
</p>
|
||||
<p>{config.app.description}</p>
|
||||
<SearchForm onSubmit={this.handleSubmit}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className={styles.counterContainer}>
|
||||
<CounterButton multireducerKey="counter1"/>
|
||||
<CounterButton multireducerKey="counter2"/>
|
||||
<CounterButton multireducerKey="counter3"/>
|
||||
</div>
|
||||
|
||||
<p>This starter boilerplate app uses the following technologies:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<del>Isomorphic</del>
|
||||
{' '}
|
||||
<a href="https://medium.com/@mjackson/universal-javascript-4761051b7ae9">Universal</a> rendering
|
||||
</li>
|
||||
<li>Both client and server make calls to load data from separate API server</li>
|
||||
<li><a href="https://github.com/facebook/react" target="_blank">React</a></li>
|
||||
<li><a href="https://github.com/rackt/react-router" target="_blank">React Router</a></li>
|
||||
<li><a href="http://expressjs.com" target="_blank">Express</a></li>
|
||||
<li><a href="http://babeljs.io" target="_blank">Babel</a> for ES6 and ES7 magic</li>
|
||||
<li><a href="http://webpack.github.io" target="_blank">Webpack</a> for bundling</li>
|
||||
<li><a href="http://webpack.github.io/docs/webpack-dev-middleware.html" target="_blank">Webpack Dev Middleware</a>
|
||||
</li>
|
||||
<li><a href="https://github.com/glenjamin/webpack-hot-middleware" target="_blank">Webpack Hot Middleware</a></li>
|
||||
<li><a href="https://github.com/rackt/redux" target="_blank">Redux</a>'s futuristic <a
|
||||
href="https://facebook.github.io/react/blog/2014/05/06/flux.html" target="_blank">Flux</a> implementation
|
||||
</li>
|
||||
<li><a href="https://github.com/gaearon/redux-devtools" target="_blank">Redux Dev Tools</a> for next
|
||||
generation DX (developer experience).
|
||||
Watch <a href="https://www.youtube.com/watch?v=xsSnOQynTHs" target="_blank">Dan Abramov's talk</a>.
|
||||
</li>
|
||||
<li><a href="https://github.com/rackt/redux-router" target="_blank">Redux Router</a> Keep
|
||||
your router state in your Redux store
|
||||
</li>
|
||||
<li><a href="http://eslint.org" target="_blank">ESLint</a> to maintain a consistent code style</li>
|
||||
<li><a href="https://github.com/erikras/redux-form" target="_blank">redux-form</a> to manage form state
|
||||
in Redux
|
||||
</li>
|
||||
<li><a href="https://github.com/erikras/multireducer" target="_blank">multireducer</a> combine several
|
||||
identical reducer states into one key-based reducer</li>
|
||||
<li><a href="https://github.com/webpack/style-loader" target="_blank">style-loader</a> and <a
|
||||
href="https://github.com/jtangelder/sass-loader" target="_blank">sass-loader</a> to allow import of
|
||||
stylesheets
|
||||
</li>
|
||||
<li><a href="https://github.com/shakacode/bootstrap-sass-loader" target="_blank">bootstrap-sass-loader</a> and <a
|
||||
href="https://github.com/gowravshekar/font-awesome-webpack" target="_blank">font-awesome-webpack</a> to customize Bootstrap and FontAwesome
|
||||
</li>
|
||||
<li><a href="http://socket.io/">socket.io</a> for real-time communication</li>
|
||||
</ul>
|
||||
|
||||
<h3>Features demonstrated in this project</h3>
|
||||
|
||||
<dl>
|
||||
<dt>Multiple components subscribing to same redux store slice</dt>
|
||||
<dd>
|
||||
The <code>App.js</code> that wraps all the pages contains an <code>InfoBar</code> component
|
||||
that fetches data from the server initially, but allows for the user to refresh the data from
|
||||
the client. <code>About.js</code> contains a <code>MiniInfoBar</code> that displays the same
|
||||
data.
|
||||
</dd>
|
||||
<dt>Server-side data loading</dt>
|
||||
<dd>
|
||||
The <Link to="/widgets">Widgets page</Link> demonstrates how to fetch data asynchronously from
|
||||
some source that is needed to complete the server-side rendering. <code>Widgets.js</code>'s
|
||||
<code>fetchData()</code> function is called before the widgets page is loaded, on either the server
|
||||
or the client, allowing all the widget data to be loaded and ready for the page to render.
|
||||
</dd>
|
||||
<dt>Data loading errors</dt>
|
||||
<dd>
|
||||
The <Link to="/widgets">Widgets page</Link> also demonstrates how to deal with data loading
|
||||
errors in Redux. The API endpoint that delivers the widget data intentionally fails 33% of
|
||||
the time to highlight this. The <code>clientMiddleware</code> sends an error action which
|
||||
the <code>widgets</code> reducer picks up and saves to the Redux state for presenting to the user.
|
||||
</dd>
|
||||
<dt>Session based login</dt>
|
||||
<dd>
|
||||
On the <Link to="/login">Login page</Link> you can submit a username which will be sent to the server
|
||||
and stored in the session. Subsequent refreshes will show that you are still logged in.
|
||||
</dd>
|
||||
<dt>Redirect after state change</dt>
|
||||
<dd>
|
||||
After you log in, you will be redirected to a Login Success page. This <strike>magic</strike> logic
|
||||
is performed in <code>componentWillReceiveProps()</code> in <code>App.js</code>, but it could
|
||||
be done in any component that listens to the appropriate store slice, via Redux's <code>@connect</code>,
|
||||
and pulls the router from the context.
|
||||
</dd>
|
||||
<dt>Auth-required views</dt>
|
||||
<dd>
|
||||
The aforementioned Login Success page is only visible to you if you are logged in. If you try
|
||||
to <Link to="/loginSuccess">go there</Link> when you are not logged in, you will be forwarded back
|
||||
to this home page. This <strike>magic</strike> logic is performed by the
|
||||
<code>onEnter</code> hook within <code>routes.js</code>.
|
||||
</dd>
|
||||
<dt>Forms</dt>
|
||||
<dd>
|
||||
The <Link to="/survey">Survey page</Link> uses the
|
||||
still-experimental <a href="https://github.com/erikras/redux-form" target="_blank">redux-form</a> to
|
||||
manage form state inside the Redux store. This includes immediate client-side validation.
|
||||
</dd>
|
||||
<dt>WebSockets / socket.io</dt>
|
||||
<dd>
|
||||
The <Link to="/chat">Chat</Link> uses the socket.io technology for real-time
|
||||
commnunication between clients. You need to <Link to="/login">login</Link> first.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h3>From the author</h3>
|
||||
|
||||
<p>
|
||||
I cobbled this together from a wide variety of similar "starter" repositories. As I post this in June 2015,
|
||||
all of these libraries are right at the bleeding edge of web development. They may fall out of fashion as
|
||||
quickly as they have come into it, but I personally believe that this stack is the future of web development
|
||||
and will survive for several years. I'm building my new projects like this, and I recommend that you do,
|
||||
too.
|
||||
</p>
|
||||
|
||||
<p>Thanks for taking the time to check this out.</p>
|
||||
|
||||
<p>– Erik Rasmussen</p>
|
||||
</div>
|
||||
<div className="container"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,60 +1,25 @@
|
|||
@import "../../theme/variables.scss";
|
||||
|
||||
.home {
|
||||
.masthead {
|
||||
.github {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
h2 {
|
||||
color: #ddd;
|
||||
font-size: 2em;
|
||||
margin: 20px;
|
||||
}
|
||||
}
|
||||
dd {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
.masthead {
|
||||
background: #2d2d2d;
|
||||
padding: 40px 20px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
.logo {
|
||||
$size: 200px;
|
||||
margin: auto;
|
||||
height: $size;
|
||||
width: $size;
|
||||
border-radius: $size / 2;
|
||||
border: 1px solid $cyan;
|
||||
box-shadow: inset 0 0 10px $cyan;
|
||||
vertical-align: middle;
|
||||
p {
|
||||
line-height: $size;
|
||||
margin: 0px;
|
||||
}
|
||||
img {
|
||||
width: 75%;
|
||||
margin: auto;
|
||||
.search {
|
||||
input {
|
||||
font-size: 10em;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
color: $cyan;
|
||||
font-size: 4em;
|
||||
}
|
||||
h2 {
|
||||
color: #ddd;
|
||||
font-size: 2em;
|
||||
margin: 20px;
|
||||
}
|
||||
a {
|
||||
color: #ddd;
|
||||
}
|
||||
p {
|
||||
margin: 10px;
|
||||
}
|
||||
.humility {
|
||||
color: $humility;
|
||||
a {
|
||||
color: $humility;
|
||||
}
|
||||
}
|
||||
.github {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.counterContainer {
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
}
|
||||
|
|
Двоичные данные
src/containers/Home/logo.png
Двоичные данные
src/containers/Home/logo.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 7.7 KiB |
|
@ -1,76 +0,0 @@
|
|||
import React, {Component, PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import DocumentMeta from 'react-document-meta';
|
||||
import {initialize} from 'redux-form';
|
||||
import {SurveyForm} from 'components';
|
||||
import config from '../../config';
|
||||
|
||||
@connect(
|
||||
() => ({}),
|
||||
{initialize})
|
||||
export default class Survey extends Component {
|
||||
static propTypes = {
|
||||
initialize: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
handleSubmit = (data) => {
|
||||
window.alert('Data submitted! ' + JSON.stringify(data));
|
||||
this.props.initialize('survey', {});
|
||||
}
|
||||
|
||||
handleInitialize = () => {
|
||||
this.props.initialize('survey', {
|
||||
name: 'Little Bobby Tables',
|
||||
email: 'bobby@gmail.com',
|
||||
occupation: 'Redux Wizard',
|
||||
currentlyEmployed: true,
|
||||
sex: 'male'
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>Survey</h1>
|
||||
<DocumentMeta title={config.app.title + ': Survey'}/>
|
||||
|
||||
<p>
|
||||
This is an example of a form in redux in which all the state is kept within the redux store.
|
||||
All the components are pure "dumb" components.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Things to notice:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>No validation errors are shown initially.</li>
|
||||
<li>Validation errors are only shown onBlur</li>
|
||||
<li>Validation errors are hidden onChange when the error is rectified</li>
|
||||
<li><code>valid</code>, <code>invalid</code>, <code>pristine</code> and <code>dirty</code> flags
|
||||
are passed with each change
|
||||
</li>
|
||||
<li><em>Except</em> when you submit the form, in which case they are shown for all invalid fields.</li>
|
||||
<li>If you click the Initialize Form button, the form will be prepopupated with some values and
|
||||
the <code>pristine</code> and <code>dirty</code> flags will be based on those values.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Pardon the use of <code>window.alert()</code>, but I wanted to keep this component stateless.
|
||||
</p>
|
||||
|
||||
<div style={{textAlign: 'center', margin: 15}}>
|
||||
<button className="btn btn-primary" onClick={this.handleInitialize}>
|
||||
<i className="fa fa-pencil"/> Initialize Form
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p>The circles to the left of the inputs correspond to flags provided by <code>redux-form</code>:
|
||||
Touched, Visited, Active, and Dirty.</p>
|
||||
|
||||
<SurveyForm onSubmit={this.handleSubmit}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
import React, {Component, PropTypes} from 'react';
|
||||
import DocumentMeta from 'react-document-meta';
|
||||
import {connect} from 'react-redux';
|
||||
import * as widgetActions from 'redux/modules/widgets';
|
||||
import {isLoaded, load as loadWidgets} from 'redux/modules/widgets';
|
||||
import {initializeWithKey} from 'redux-form';
|
||||
import connectData from 'helpers/connectData';
|
||||
import { WidgetForm } from 'components';
|
||||
import config from '../../config';
|
||||
|
||||
function fetchDataDeferred(getState, dispatch) {
|
||||
if (!isLoaded(getState())) {
|
||||
return dispatch(loadWidgets());
|
||||
}
|
||||
}
|
||||
|
||||
@connectData(null, fetchDataDeferred)
|
||||
@connect(
|
||||
state => ({
|
||||
widgets: state.widgets.data,
|
||||
editing: state.widgets.editing,
|
||||
error: state.widgets.error,
|
||||
loading: state.widgets.loading
|
||||
}),
|
||||
{...widgetActions, initializeWithKey })
|
||||
export default class Widgets extends Component {
|
||||
static propTypes = {
|
||||
widgets: PropTypes.array,
|
||||
error: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
initializeWithKey: PropTypes.func.isRequired,
|
||||
editing: PropTypes.object.isRequired,
|
||||
load: PropTypes.func.isRequired,
|
||||
editStart: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const handleEdit = (widget) => {
|
||||
const {editStart} = this.props; // eslint-disable-line no-shadow
|
||||
return () => editStart(String(widget.id));
|
||||
};
|
||||
const {widgets, error, editing, loading, load} = this.props;
|
||||
let refreshClassName = 'fa fa-refresh';
|
||||
if (loading) {
|
||||
refreshClassName += ' fa-spin';
|
||||
}
|
||||
const styles = require('./Widgets.scss');
|
||||
return (
|
||||
<div className={styles.widgets + ' container'}>
|
||||
<h1>
|
||||
Widgets
|
||||
<button className={styles.refreshBtn + ' btn btn-success'} onClick={load}>
|
||||
<i className={refreshClassName}/> {' '} Reload Widgets
|
||||
</button>
|
||||
</h1>
|
||||
<DocumentMeta title={config.app.title + ': Widgets'}/>
|
||||
<p>
|
||||
If you hit refresh on your browser, the data loading will take place on the server before the page is returned.
|
||||
If you navigated here from another page, the data was fetched from the client after the route transition.
|
||||
This uses the static method <code>fetchDataDeferred</code>. To block a route transition until some data is loaded, use <code>fetchData</code>.
|
||||
To always render before loading data, even on the server, use <code>componentDidMount</code>.
|
||||
</p>
|
||||
<p>
|
||||
This widgets are stored in your session, so feel free to edit it and refresh.
|
||||
</p>
|
||||
{error &&
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<span className="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
|
||||
{' '}
|
||||
{error}
|
||||
</div>}
|
||||
{widgets && widgets.length &&
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.idCol}>ID</th>
|
||||
<th className={styles.colorCol}>Color</th>
|
||||
<th className={styles.sprocketsCol}>Sprockets</th>
|
||||
<th className={styles.ownerCol}>Owner</th>
|
||||
<th className={styles.buttonCol}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
widgets.map((widget) => editing[widget.id] ?
|
||||
<WidgetForm formKey={String(widget.id)} key={String(widget.id)} initialValues={widget}/> :
|
||||
<tr key={widget.id}>
|
||||
<td className={styles.idCol}>{widget.id}</td>
|
||||
<td className={styles.colorCol}>{widget.color}</td>
|
||||
<td className={styles.sprocketsCol}>{widget.sprocketCount}</td>
|
||||
<td className={styles.ownerCol}>{widget.owner}</td>
|
||||
<td className={styles.buttonCol}>
|
||||
<button className="btn btn-primary" onClick={handleEdit(widget)}>
|
||||
<i className="fa fa-pencil"/> Edit
|
||||
</button>
|
||||
</td>
|
||||
</tr>)
|
||||
}
|
||||
</tbody>
|
||||
</table>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
.widgets {
|
||||
.refreshBtn {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.idCol {
|
||||
width: 5%;
|
||||
}
|
||||
.colorCol {
|
||||
width: 20%;
|
||||
}
|
||||
.sprocketsCol {
|
||||
width: 20%;
|
||||
text-align: right;
|
||||
input {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.ownerCol {
|
||||
width: 30%;
|
||||
}
|
||||
.buttonCol {
|
||||
width: 25%;
|
||||
:global(.btn) {
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
tr.saving {
|
||||
opacity: 0.8;
|
||||
:global(.btn) {
|
||||
&[disabled] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,5 @@
|
|||
export App from './App/App';
|
||||
export Chat from './Chat/Chat';
|
||||
export Home from './Home/Home';
|
||||
export Widgets from './Widgets/Widgets';
|
||||
export About from './About/About';
|
||||
export Login from './Login/Login';
|
||||
export LoginSuccess from './LoginSuccess/LoginSuccess';
|
||||
export Survey from './Survey/Survey';
|
||||
export NotFound from './NotFound/NotFound';
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
const INCREMENT = 'redux-example/counter/INCREMENT';
|
||||
|
||||
const initialState = {
|
||||
count: 0
|
||||
};
|
||||
|
||||
export default function reducer(state = initialState, action = {}) {
|
||||
switch (action.type) {
|
||||
case INCREMENT:
|
||||
const {count} = state;
|
||||
return {
|
||||
count: count + 1
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function increment() {
|
||||
return {
|
||||
type: INCREMENT
|
||||
};
|
||||
}
|
|
@ -1,22 +1,13 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import multireducer from 'multireducer';
|
||||
import { routerStateReducer } from 'redux-router';
|
||||
|
||||
import auth from './auth';
|
||||
import counter from './counter';
|
||||
import {reducer as form} from 'redux-form';
|
||||
import info from './info';
|
||||
import widgets from './widgets';
|
||||
|
||||
export default combineReducers({
|
||||
router: routerStateReducer,
|
||||
auth,
|
||||
form,
|
||||
multireducer: multireducer({
|
||||
counter1: counter,
|
||||
counter2: counter,
|
||||
counter3: counter
|
||||
}),
|
||||
info,
|
||||
widgets
|
||||
});
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
const LOAD = 'redux-example/widgets/LOAD';
|
||||
const LOAD_SUCCESS = 'redux-example/widgets/LOAD_SUCCESS';
|
||||
const LOAD_FAIL = 'redux-example/widgets/LOAD_FAIL';
|
||||
const EDIT_START = 'redux-example/widgets/EDIT_START';
|
||||
const EDIT_STOP = 'redux-example/widgets/EDIT_STOP';
|
||||
const SAVE = 'redux-example/widgets/SAVE';
|
||||
const SAVE_SUCCESS = 'redux-example/widgets/SAVE_SUCCESS';
|
||||
const SAVE_FAIL = 'redux-example/widgets/SAVE_FAIL';
|
||||
|
||||
const initialState = {
|
||||
loaded: false,
|
||||
editing: {},
|
||||
saveError: {}
|
||||
};
|
||||
|
||||
export default function reducer(state = initialState, action = {}) {
|
||||
switch (action.type) {
|
||||
case LOAD:
|
||||
return {
|
||||
...state,
|
||||
loading: true
|
||||
};
|
||||
case LOAD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded: true,
|
||||
data: action.result,
|
||||
error: null
|
||||
};
|
||||
case LOAD_FAIL:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
data: null,
|
||||
error: action.error
|
||||
};
|
||||
case EDIT_START:
|
||||
return {
|
||||
...state,
|
||||
editing: {
|
||||
...state.editing,
|
||||
[action.id]: true
|
||||
}
|
||||
};
|
||||
case EDIT_STOP:
|
||||
return {
|
||||
...state,
|
||||
editing: {
|
||||
...state.editing,
|
||||
[action.id]: false
|
||||
}
|
||||
};
|
||||
case SAVE:
|
||||
return state; // 'saving' flag handled by redux-form
|
||||
case SAVE_SUCCESS:
|
||||
const data = [...state.data];
|
||||
data[action.result.id - 1] = action.result;
|
||||
return {
|
||||
...state,
|
||||
data: data,
|
||||
editing: {
|
||||
...state.editing,
|
||||
[action.id]: false
|
||||
},
|
||||
saveError: {
|
||||
...state.saveError,
|
||||
[action.id]: null
|
||||
}
|
||||
};
|
||||
case SAVE_FAIL:
|
||||
return typeof action.error === 'string' ? {
|
||||
...state,
|
||||
saveError: {
|
||||
...state.saveError,
|
||||
[action.id]: action.error
|
||||
}
|
||||
} : state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function isLoaded(globalState) {
|
||||
return globalState.widgets && globalState.widgets.loaded;
|
||||
}
|
||||
|
||||
export function load() {
|
||||
return {
|
||||
types: [LOAD, LOAD_SUCCESS, LOAD_FAIL],
|
||||
promise: (client) => client.get('/widget/load/param1/param2') // params not used, just shown as demonstration
|
||||
};
|
||||
}
|
||||
|
||||
export function save(widget) {
|
||||
return {
|
||||
types: [SAVE, SAVE_SUCCESS, SAVE_FAIL],
|
||||
id: widget.id,
|
||||
promise: (client) => client.post('/widget/update', {
|
||||
data: widget
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
export function editStart(id) {
|
||||
return { type: EDIT_START, id };
|
||||
}
|
||||
|
||||
export function editStop(id) {
|
||||
return { type: EDIT_STOP, id };
|
||||
}
|
|
@ -3,13 +3,9 @@ import {IndexRoute, Route} from 'react-router';
|
|||
import { isLoaded as isAuthLoaded, load as loadAuth } from 'redux/modules/auth';
|
||||
import {
|
||||
App,
|
||||
Chat,
|
||||
Home,
|
||||
Widgets,
|
||||
About,
|
||||
Login,
|
||||
LoginSuccess,
|
||||
Survey,
|
||||
NotFound,
|
||||
} from 'containers';
|
||||
|
||||
|
@ -41,15 +37,11 @@ export default (store) => {
|
|||
|
||||
{ /* Routes requiring login */ }
|
||||
<Route onEnter={requireLogin}>
|
||||
<Route path="chat" component={Chat}/>
|
||||
<Route path="loginSuccess" component={LoginSuccess}/>
|
||||
</Route>
|
||||
|
||||
{ /* Routes */ }
|
||||
<Route path="about" component={About}/>
|
||||
<Route path="login" component={Login}/>
|
||||
<Route path="survey" component={Survey}/>
|
||||
<Route path="widgets" component={Widgets}/>
|
||||
|
||||
{ /* Catch all route */ }
|
||||
<Route path="*" component={NotFound} status={404} />
|
||||
|
|
Загрузка…
Ссылка в новой задаче