Remove boilerplate add search form

This commit is contained in:
Stuart Colville 2015-12-08 17:40:09 -05:00
Родитель 1cc9c60fda
Коммит 3f12efa66b
29 изменённых файлов: 71 добавлений и 1106 удалений

Просмотреть файл

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

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 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

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 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} />