├── .gitignore
├── README.md
├── bower.json
├── fonts
├── Lato-Light.eot
├── Lato-Light.html
├── Lato-Light.ttf
├── Lato-Light.woff
├── Lato-Regular.eot
├── Lato-Regular.html
├── Lato-Regular.ttf
├── Lato-Regular.woff
├── Lato-Thin.eot
├── Lato-Thin.html
├── Lato-Thin.ttf
└── Lato-Thin.woff
├── gulpfile.js
├── index.html
├── package.json
├── scripts
├── actions
│ ├── RouteActionCreators.react.jsx
│ ├── ServerActionCreators.react.jsx
│ ├── SessionActionCreators.react.jsx
│ └── StoryActionCreators.react.jsx
├── app.jsx
├── components
│ ├── Header.react.jsx
│ ├── SmallApp.react.jsx
│ ├── common
│ │ └── ErrorNotice.react.jsx
│ ├── session
│ │ ├── LoginPage.react.jsx
│ │ └── SignupPage.react.jsx
│ └── stories
│ │ ├── StoriesPage.react.jsx
│ │ ├── StoryNew.react.jsx
│ │ └── StoryPage.react.jsx
├── constants
│ └── SmallConstants.js
├── dispatcher
│ └── SmallAppDispatcher.js
├── routes.jsx
├── stores
│ ├── RouteStore.react.jsx
│ ├── SessionStore.react.jsx
│ └── StoryStore.react.jsx
└── utils
│ └── WebAPIUtils.js
└── styles
├── fonts.scss
├── globals.scss
├── main.scss
└── spinner.scss
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .sass-cache
3 | dist
4 | bower_components
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Small
2 | This is a sample project used to illustrate the Flux architecture with React. Checkout the full article [here](http://fancypixel.github.io/blog/2015/01/28/react-plus-flux-backed-by-rails-api/)
3 |
4 | ## Setup
5 | ```
6 | npm install
7 | bower install
8 | gulp watch
9 | ```
10 |
11 | # MIT License
12 |
13 | Copyright (c) 2015 Fancy Pixel. All rights reserved.
14 |
15 | Permission is hereby granted, free of charge, to any person obtaining a
16 | copy of this software and associated documentation files (the "Software"),
17 | to deal in the Software without restriction, including
18 | without limitation the rights to use, copy, modify, merge, publish,
19 | distribute, sublicense, and/or sell copies of the Software, and to
20 | permit persons to whom the Software is furnished to do so, subject to
21 | the following conditions:
22 |
23 | The above copyright notice and this permission notice shall be included
24 | in all copies or substantial portions of the Software.
25 |
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
27 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
29 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
30 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
31 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
32 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 |
34 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "carrot-frontend",
3 | "version": "0.0.2",
4 | "dependencies": {
5 | "normalize.css": "~3.0.2",
6 | "foundation": "~5.5.0",
7 | "modernizr": "^2.8.3"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/fonts/Lato-Light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FancyPixel/small-frontend/8369b6088893b32d24bcef65c7ad5a8f69d2eda7/fonts/Lato-Light.eot
--------------------------------------------------------------------------------
/fonts/Lato-Light.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Lato Light - Web Font Specimen
7 |
8 |
11 |
12 |
13 | The quick brown fox jumps over the lazy dog. $123.45!
14 |
15 |
16 |
--------------------------------------------------------------------------------
/fonts/Lato-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FancyPixel/small-frontend/8369b6088893b32d24bcef65c7ad5a8f69d2eda7/fonts/Lato-Light.ttf
--------------------------------------------------------------------------------
/fonts/Lato-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FancyPixel/small-frontend/8369b6088893b32d24bcef65c7ad5a8f69d2eda7/fonts/Lato-Light.woff
--------------------------------------------------------------------------------
/fonts/Lato-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FancyPixel/small-frontend/8369b6088893b32d24bcef65c7ad5a8f69d2eda7/fonts/Lato-Regular.eot
--------------------------------------------------------------------------------
/fonts/Lato-Regular.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Lato Regular - Web Font Specimen
7 |
8 |
11 |
12 |
13 | The quick brown fox jumps over the lazy dog. $123.45!
14 |
15 |
16 |
--------------------------------------------------------------------------------
/fonts/Lato-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FancyPixel/small-frontend/8369b6088893b32d24bcef65c7ad5a8f69d2eda7/fonts/Lato-Regular.ttf
--------------------------------------------------------------------------------
/fonts/Lato-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FancyPixel/small-frontend/8369b6088893b32d24bcef65c7ad5a8f69d2eda7/fonts/Lato-Regular.woff
--------------------------------------------------------------------------------
/fonts/Lato-Thin.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FancyPixel/small-frontend/8369b6088893b32d24bcef65c7ad5a8f69d2eda7/fonts/Lato-Thin.eot
--------------------------------------------------------------------------------
/fonts/Lato-Thin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Lato Thin - Web Font Specimen
7 |
8 |
11 |
12 |
13 | The quick brown fox jumps over the lazy dog. $123.45!
14 |
15 |
16 |
--------------------------------------------------------------------------------
/fonts/Lato-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FancyPixel/small-frontend/8369b6088893b32d24bcef65c7ad5a8f69d2eda7/fonts/Lato-Thin.ttf
--------------------------------------------------------------------------------
/fonts/Lato-Thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FancyPixel/small-frontend/8369b6088893b32d24bcef65c7ad5a8f69d2eda7/fonts/Lato-Thin.woff
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp'),
4 | gulpFilter = require('gulp-filter'),
5 | flatten = require('gulp-flatten'),
6 | mainBowerFiles = require('main-bower-files'),
7 | rename = require("gulp-rename"),
8 | minifycss = require('gulp-minify-css'),
9 | changed = require('gulp-changed'),
10 | sass = require('gulp-sass'),
11 | csso = require('gulp-csso'),
12 | autoprefixer = require('gulp-autoprefixer'),
13 | browserify = require('browserify'),
14 | watchify = require('watchify'),
15 | source = require('vinyl-source-stream'),
16 | buffer = require('vinyl-buffer'),
17 | reactify = require('reactify'),
18 | uglify = require('gulp-uglify'),
19 | del = require('del'),
20 | notify = require('gulp-notify'),
21 | browserSync = require('browser-sync'),
22 | reload = browserSync.reload,
23 | p = {
24 | jsx: './scripts/app.jsx',
25 | scss: 'styles/main.scss',
26 | scssSource: 'styles/*',
27 | font: 'fonts/*',
28 | bundle: 'app.js',
29 | distJs: 'dist/js',
30 | distCss: 'dist/css',
31 | distFont: 'dist/fonts'
32 | };
33 |
34 | gulp.task('clean', function(cb) {
35 | return del(['dist'], cb);
36 | });
37 |
38 | gulp.task('browserSync', function() {
39 | browserSync({
40 | notify: false,
41 | server: {
42 | baseDir: './'
43 | }
44 | })
45 | });
46 |
47 | gulp.task('watchify', function() {
48 | var bundler = watchify(browserify(p.jsx, watchify.args));
49 |
50 | function rebundle() {
51 | return bundler
52 | .bundle()
53 | .on('error', notify.onError())
54 | .pipe(source(p.bundle))
55 | .pipe(gulp.dest(p.distJs))
56 | .pipe(reload({stream: true}));
57 | }
58 |
59 | bundler.transform(reactify)
60 | .on('update', rebundle);
61 | return rebundle();
62 | });
63 |
64 | gulp.task('browserify', function() {
65 | browserify(p.jsx)
66 | .transform(reactify)
67 | .bundle()
68 | .pipe(source(p.bundle))
69 | .pipe(buffer())
70 | .pipe(uglify())
71 | .pipe(gulp.dest(p.distJs));
72 | });
73 |
74 | gulp.task('fonts', function() {
75 | return gulp.src(p.font)
76 | .pipe(gulp.dest(p.distFont));
77 | });
78 |
79 | gulp.task('styles', function() {
80 | return gulp.src(p.scss)
81 | .pipe(changed(p.distCss))
82 | .pipe(sass({errLogToConsole: true}))
83 | .on('error', notify.onError())
84 | .pipe(autoprefixer({
85 | browsers: ['last 1 version']
86 | }))
87 | .pipe(csso())
88 | .pipe(gulp.dest(p.distCss))
89 | .pipe(reload({stream: true}));
90 | });
91 |
92 | // Ugly hack to bring resources in
93 | gulp.task('modernizr', function() {
94 | return gulp.src('bower_components/modernizr/modernizr.js')
95 | .pipe(gulp.dest(p.distJs));
96 | });
97 | gulp.task('foundation-js', function() {
98 | return gulp.src('bower_components/foundation/js/foundation.min.js')
99 | .pipe(gulp.dest(p.distJs));
100 | });
101 | gulp.task('foundation-css', function() {
102 | return gulp.src('bower_components/foundation/css/foundation.min.css')
103 | .pipe(gulp.dest(p.distCss));
104 | });
105 |
106 | gulp.task('bower-libs', function() {
107 | var jsFilter = gulpFilter('*.js', {restore: true});
108 | var cssFilter = gulpFilter('*.css', {restore: true});
109 | var fontFilter = gulpFilter(['*.eot', '*.woff', '*.svg', '*.ttf']);
110 |
111 | return gulp.src(mainBowerFiles())
112 |
113 | // JS from bower_components
114 | .pipe(jsFilter)
115 | .pipe(gulp.dest(p.distJs))
116 | .pipe(uglify())
117 | .pipe(rename({
118 | suffix: ".min"
119 | }))
120 | .pipe(gulp.dest(p.distJs))
121 | .pipe(jsFilter.restore)
122 |
123 | // css from bower_components, minified
124 | .pipe(cssFilter)
125 | .pipe(gulp.dest(p.distCss))
126 | .pipe(minifycss())
127 | .pipe(rename({
128 | suffix: ".min"
129 | }))
130 | .pipe(gulp.dest(p.distCss))
131 | .pipe(cssFilter.restore)
132 |
133 | // font files from bower_components
134 | .pipe(fontFilter)
135 | .pipe(flatten())
136 | .pipe(gulp.dest(p.distFont));
137 | });
138 |
139 | gulp.task('libs', function() {
140 | gulp.start(['modernizr', 'foundation-css', 'foundation-js', 'bower-libs', 'fonts']);
141 | });
142 |
143 | gulp.task('watchTask', function() {
144 | gulp.watch(p.scssSource, ['styles']);
145 | });
146 |
147 | gulp.task('watch', ['clean'], function() {
148 | gulp.start(['libs', 'browserSync', 'watchTask', 'watchify', 'styles']);
149 | });
150 |
151 | gulp.task('build', ['clean'], function() {
152 | process.env.NODE_ENV = 'production';
153 | gulp.start(['libs', 'browserify', 'styles']);
154 | });
155 |
156 | gulp.task('default', function() {
157 | console.log('Run "gulp watch or gulp build"');
158 | });
159 |
160 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Small
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "small-frontend",
3 | "version": "0.0.0",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/FancyPixel/small-frontend.git"
7 | },
8 | "license": "MIT",
9 | "dependencies": {
10 | "flux": "^2.0.0",
11 | "keymirror": "~0.1.0",
12 | "moment": "^2.10.6",
13 | "object-assign": "^4.0.1",
14 | "react": "^0.13.0",
15 | "superagent": "^1.4.0"
16 | },
17 | "devDependencies": {
18 | "browser-sync": "~2.9.6",
19 | "browserify": "^11.2.0",
20 | "del": "^2.0.2",
21 | "es6ify": "^1.6.0",
22 | "gulp": "^3.8.10",
23 | "gulp-autoprefixer": "~2.3.1",
24 | "gulp-bower": "^0.0.10",
25 | "gulp-cache": "~0.3.0",
26 | "gulp-changed": "^1.0.0",
27 | "gulp-csso": "^1.0.0",
28 | "gulp-filter": "~3.0.1",
29 | "gulp-flatten": "0.2.0",
30 | "gulp-imagemin": "latest",
31 | "gulp-jest": "~0.4.0",
32 | "gulp-minify-css": "^1.2.1",
33 | "gulp-notify": "^2.1.0",
34 | "gulp-rename": "^1.2.0",
35 | "gulp-sass": "^2.0.4",
36 | "gulp-size": "~2.0.0",
37 | "gulp-uglify": "^1.0.1",
38 | "gulp-util": "~3.0.1",
39 | "jest": "latest",
40 | "main-bower-files": "^2.5.0",
41 | "react-router": "^0.13.3",
42 | "reactify": "^1.1.1",
43 | "vinyl-buffer": "^1.0.0",
44 | "vinyl-source-stream": "^1.0.0",
45 | "watchify": "^3.4.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/scripts/actions/RouteActionCreators.react.jsx:
--------------------------------------------------------------------------------
1 | var SmallAppDispatcher = require('../dispatcher/SmallAppDispatcher.js');
2 | var SmallConstants = require('../constants/SmallConstants.js');
3 |
4 | var ActionTypes = SmallConstants.ActionTypes;
5 |
6 | module.exports = {
7 |
8 | redirect: function(route) {
9 | SmallAppDispatcher.handleViewAction({
10 | type: ActionTypes.REDIRECT,
11 | route: route
12 | });
13 | }
14 |
15 | };
16 |
17 |
18 |
--------------------------------------------------------------------------------
/scripts/actions/ServerActionCreators.react.jsx:
--------------------------------------------------------------------------------
1 | var SmallAppDispatcher = require('../dispatcher/SmallAppDispatcher.js');
2 | var SmallConstants = require('../constants/SmallConstants.js');
3 |
4 | var ActionTypes = SmallConstants.ActionTypes;
5 |
6 | module.exports = {
7 |
8 | receiveLogin: function(json, errors) {
9 | SmallAppDispatcher.handleServerAction({
10 | type: ActionTypes.LOGIN_RESPONSE,
11 | json: json,
12 | errors: errors
13 | });
14 | },
15 |
16 | receiveStories: function(json) {
17 | SmallAppDispatcher.handleServerAction({
18 | type: ActionTypes.RECEIVE_STORIES,
19 | json: json
20 | });
21 | },
22 |
23 | receiveStory: function(json) {
24 | SmallAppDispatcher.handleServerAction({
25 | type: ActionTypes.RECEIVE_STORY,
26 | json: json
27 | });
28 | },
29 |
30 | receiveCreatedStory: function(json, errors) {
31 | SmallAppDispatcher.handleServerAction({
32 | type: ActionTypes.RECEIVE_CREATED_STORY,
33 | json: json,
34 | errors: errors
35 | });
36 | }
37 |
38 | };
39 |
40 |
--------------------------------------------------------------------------------
/scripts/actions/SessionActionCreators.react.jsx:
--------------------------------------------------------------------------------
1 | var SmallAppDispatcher = require('../dispatcher/SmallAppDispatcher.js');
2 | var SmallConstants = require('../constants/SmallConstants.js');
3 | var WebAPIUtils = require('../utils/WebAPIUtils.js');
4 |
5 | var ActionTypes = SmallConstants.ActionTypes;
6 |
7 | module.exports = {
8 |
9 | signup: function(email, username, password, passwordConfirmation) {
10 | SmallAppDispatcher.handleViewAction({
11 | type: ActionTypes.SIGNUP_REQUEST,
12 | email: email,
13 | username: username,
14 | password: password,
15 | passwordConfirmation: passwordConfirmation
16 | });
17 | WebAPIUtils.signup(email, username, password, passwordConfirmation);
18 | },
19 |
20 | login: function(email, password) {
21 | SmallAppDispatcher.handleViewAction({
22 | type: ActionTypes.LOGIN_REQUEST,
23 | email: email,
24 | password: password
25 | });
26 | WebAPIUtils.login(email, password);
27 | },
28 |
29 | logout: function() {
30 | SmallAppDispatcher.handleViewAction({
31 | type: ActionTypes.LOGOUT
32 | });
33 | }
34 |
35 | };
36 |
37 |
--------------------------------------------------------------------------------
/scripts/actions/StoryActionCreators.react.jsx:
--------------------------------------------------------------------------------
1 | var SmallAppDispatcher = require('../dispatcher/SmallAppDispatcher.js');
2 | var SmallConstants = require('../constants/SmallConstants.js');
3 | var WebAPIUtils = require('../utils/WebAPIUtils.js');
4 |
5 | var ActionTypes = SmallConstants.ActionTypes;
6 |
7 | module.exports = {
8 |
9 | loadStories: function() {
10 | SmallAppDispatcher.handleViewAction({
11 | type: ActionTypes.LOAD_STORIES
12 | });
13 | WebAPIUtils.loadStories();
14 | },
15 |
16 | loadStory: function(storyId) {
17 | SmallAppDispatcher.handleViewAction({
18 | type: ActionTypes.LOAD_STORY,
19 | storyId: storyId
20 | });
21 | WebAPIUtils.loadStory(storyId);
22 | },
23 |
24 | createStory: function(title, body) {
25 | SmallAppDispatcher.handleViewAction({
26 | type: ActionTypes.CREATE_STORY,
27 | title: title,
28 | body: body
29 | });
30 | WebAPIUtils.createStory(title, body);
31 | }
32 |
33 | };
34 |
35 |
--------------------------------------------------------------------------------
/scripts/app.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var router = require('./stores/RouteStore.react.jsx').getRouter();
3 | window.React = React;
4 |
5 | router.run(function (Handler, state) {
6 | React.render( , document.getElementById('content'));
7 | });
8 |
--------------------------------------------------------------------------------
/scripts/components/Header.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Router = require('react-router');
3 | var Link = Router.Link;
4 | var ReactPropTypes = React.PropTypes;
5 | var SessionActionCreators = require('../actions/SessionActionCreators.react.jsx');
6 |
7 | var Header = React.createClass({
8 |
9 | propTypes: {
10 | isLoggedIn: ReactPropTypes.bool,
11 | email: ReactPropTypes.string
12 | },
13 | logout: function(e) {
14 | e.preventDefault();
15 | SessionActionCreators.logout();
16 | },
17 | render: function() {
18 | var rightNav = this.props.isLoggedIn ? (
19 |
27 | ) : (
28 |
29 | Login
30 | Sign up
31 |
32 | );
33 |
34 | var leftNav = this.props.isLoggedIn ? (
35 |
38 | ) : (
39 |
40 | );
41 |
42 | return (
43 |
44 |
45 |
46 |
47 |
48 | Menu
49 |
50 |
51 |
52 | {rightNav}
53 | {leftNav}
54 |
55 |
56 | );
57 | }
58 | });
59 |
60 | module.exports = Header;
61 |
62 |
--------------------------------------------------------------------------------
/scripts/components/SmallApp.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var RouteHandler = require('react-router').RouteHandler;
3 | var Header = require('../components/Header.react.jsx');
4 | var SessionStore = require('../stores/SessionStore.react.jsx');
5 | var RouteStore = require('../stores/RouteStore.react.jsx');
6 |
7 | function getStateFromStores() {
8 | return {
9 | isLoggedIn: SessionStore.isLoggedIn(),
10 | email: SessionStore.getEmail()
11 | };
12 | }
13 |
14 | var SmallApp = React.createClass({
15 |
16 | getInitialState: function() {
17 | return getStateFromStores();
18 | },
19 |
20 | componentDidMount: function() {
21 | SessionStore.addChangeListener(this._onChange);
22 | },
23 |
24 | componentWillUnmount: function() {
25 | SessionStore.removeChangeListener(this._onChange);
26 | },
27 |
28 | _onChange: function() {
29 | this.setState(getStateFromStores());
30 | },
31 |
32 | render: function() {
33 | return (
34 |
35 |
38 |
39 |
40 | );
41 | }
42 |
43 | });
44 |
45 | module.exports = SmallApp;
46 |
47 |
--------------------------------------------------------------------------------
/scripts/components/common/ErrorNotice.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | var ErrorNotice = React.createClass({
4 | render: function() {
5 | return (
6 |
7 |
8 | {this.props.errors.map(function(error, index){
9 | return {error} ;
10 | })}
11 |
12 |
13 | );
14 | }
15 | });
16 |
17 | module.exports = ErrorNotice;
18 |
19 |
--------------------------------------------------------------------------------
/scripts/components/session/LoginPage.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var SessionActionCreators = require('../../actions/SessionActionCreators.react.jsx');
3 | var SessionStore = require('../../stores/SessionStore.react.jsx');
4 | var ErrorNotice = require('../../components/common/ErrorNotice.react.jsx');
5 |
6 | var LoginPage = React.createClass({
7 |
8 | getInitialState: function() {
9 | return { errors: [] };
10 | },
11 |
12 | componentDidMount: function() {
13 | SessionStore.addChangeListener(this._onChange);
14 | },
15 |
16 | componentWillUnmount: function() {
17 | SessionStore.removeChangeListener(this._onChange);
18 | },
19 |
20 | _onChange: function() {
21 | this.setState({ errors: SessionStore.getErrors() });
22 | },
23 |
24 | _onSubmit: function(e) {
25 | e.preventDefault();
26 | this.setState({ errors: [] });
27 | var email = this.refs.email.getDOMNode().value;
28 | var password = this.refs.password.getDOMNode().value;
29 | SessionActionCreators.login(email, password);
30 | },
31 |
32 | render: function() {
33 | var errors = (this.state.errors.length > 0) ? :
;
34 | return (
35 |
53 | );
54 | }
55 | });
56 |
57 | module.exports = LoginPage;
58 |
59 |
--------------------------------------------------------------------------------
/scripts/components/session/SignupPage.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var SessionActionCreators = require('../../actions/SessionActionCreators.react.jsx');
3 | var SessionStore = require('../../stores/SessionStore.react.jsx');
4 | var ErrorNotice = require('../../components/common/ErrorNotice.react.jsx');
5 |
6 | var SignupPage = React.createClass({
7 |
8 | getInitialState: function() {
9 | return { errors: [] };
10 | },
11 |
12 | componentDidMount: function() {
13 | SessionStore.addChangeListener(this._onChange);
14 | },
15 |
16 | componentWillUnmount: function() {
17 | SessionStore.removeChangeListener(this._onChange);
18 | },
19 |
20 | _onChange: function() {
21 | this.setState({ errors: SessionStore.getErrors() });
22 | },
23 |
24 | _onSubmit: function(e) {
25 | e.preventDefault();
26 | this.setState({ errors: [] });
27 | var email = this.refs.email.getDOMNode().value;
28 | var username = this.refs.username.getDOMNode().value;
29 | var password = this.refs.password.getDOMNode().value;
30 | var passwordConfirmation = this.refs.passwordConfirmation.getDOMNode().value;
31 | if (password !== passwordConfirmation) {
32 | this.setState({ errors: ['Password and password confirmation should match']});
33 | } else {
34 | SessionActionCreators.signup(email, username, password, passwordConfirmation);
35 | }
36 | },
37 |
38 | render: function() {
39 | var errors = (this.state.errors.length > 0) ? :
;
40 | return (
41 |
67 | );
68 | }
69 | });
70 |
71 | module.exports = SignupPage;
72 |
73 |
--------------------------------------------------------------------------------
/scripts/components/stories/StoriesPage.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var WebAPIUtils = require('../../utils/WebAPIUtils.js');
3 | var StoryStore = require('../../stores/StoryStore.react.jsx');
4 | var ErrorNotice = require('../../components/common/ErrorNotice.react.jsx');
5 | var StoryActionCreators = require('../../actions/StoryActionCreators.react.jsx');
6 | var Router = require('react-router');
7 | var Link = Router.Link;
8 | var moment = require('moment');
9 |
10 | var StoriesPage = React.createClass({
11 |
12 | getInitialState: function() {
13 | return {
14 | stories: StoryStore.getAllStories(),
15 | errors: []
16 | };
17 | },
18 |
19 | componentDidMount: function() {
20 | StoryStore.addChangeListener(this._onChange);
21 | StoryActionCreators.loadStories();
22 | },
23 |
24 | componentWillUnmount: function() {
25 | StoryStore.removeChangeListener(this._onChange);
26 | },
27 |
28 | _onChange: function() {
29 | this.setState({
30 | stories: StoryStore.getAllStories(),
31 | errors: StoryStore.getErrors()
32 | });
33 | },
34 |
35 | render: function() {
36 | var errors = (this.state.errors.length > 0) ? :
;
37 | return (
38 |
39 | {errors}
40 |
41 |
42 |
43 |
44 | );
45 | }
46 | });
47 |
48 | var StoryItem = React.createClass({
49 | render: function() {
50 | return (
51 |
52 |
53 |
54 | {this.props.story.title}
55 |
56 |
57 | {this.props.story['abstract']}...
58 | {this.props.story.user.username}
59 | - {moment(this.props.story.created_at).fromNow()}
60 |
61 | );
62 | }
63 | });
64 |
65 | var StoriesList = React.createClass({
66 | render: function() {
67 | return (
68 |
69 | {this.props.stories.map(function(story, index){
70 | return
71 | })}
72 |
73 | );
74 | }
75 | });
76 |
77 | module.exports = StoriesPage;
78 |
79 |
--------------------------------------------------------------------------------
/scripts/components/stories/StoryNew.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var SmallAppDispatcher = require('../../dispatcher/SmallAppDispatcher.js');
3 | var SmallConstants = require('../../constants/SmallConstants.js');
4 | var WebAPIUtils = require('../../utils/WebAPIUtils.js');
5 | var SessionStore = require('../../stores/SessionStore.react.jsx');
6 | var StoryActionCreators = require('../../actions/StoryActionCreators.react.jsx');
7 | var RouteActionCreators = require('../../actions/RouteActionCreators.react.jsx');
8 |
9 | var StoryNew = React.createClass({
10 |
11 | componentDidMount: function() {
12 | if (!SessionStore.isLoggedIn()) {
13 | RouteActionCreators.redirect('app');
14 | }
15 | },
16 |
17 | _onSubmit: function(e) {
18 | e.preventDefault();
19 | var title = this.refs.title.getDOMNode().value;
20 | var body = this.refs.body.getDOMNode().value;
21 | StoryActionCreators.createStory(title, body);
22 | },
23 |
24 | render: function() {
25 | return (
26 |
39 | );
40 | }
41 |
42 | });
43 |
44 | module.exports = StoryNew;
45 |
46 |
--------------------------------------------------------------------------------
/scripts/components/stories/StoryPage.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var WebAPIUtils = require('../../utils/WebAPIUtils.js');
3 | var StoryStore = require('../../stores/StoryStore.react.jsx');
4 | var StoryActionCreators = require('../../actions/StoryActionCreators.react.jsx');
5 | var State = require('react-router').State;
6 |
7 | var StoryPage = React.createClass({
8 |
9 | mixins: [ State ],
10 |
11 | getInitialState: function() {
12 | return {
13 | story: StoryStore.getStory(),
14 | errors: []
15 | };
16 | },
17 |
18 | componentDidMount: function() {
19 | StoryStore.addChangeListener(this._onChange);
20 | StoryActionCreators.loadStory(this.getParams().storyId);
21 | },
22 |
23 | componentWillUnmount: function() {
24 | StoryStore.removeChangeListener(this._onChange);
25 | },
26 |
27 | _onChange: function() {
28 | this.setState({
29 | story: StoryStore.getStory(),
30 | errors: StoryStore.getErrors()
31 | });
32 | },
33 |
34 | render: function() {
35 | return (
36 |
37 |
{this.state.story.title}
38 |
{this.state.story.body}
39 |
{this.state.story.user.username}
40 |
41 | );
42 | }
43 |
44 | });
45 |
46 | module.exports = StoryPage;
47 |
48 |
--------------------------------------------------------------------------------
/scripts/constants/SmallConstants.js:
--------------------------------------------------------------------------------
1 | var keyMirror = require('keymirror');
2 |
3 | var APIRoot = "http://localhost:3000";
4 |
5 | module.exports = {
6 |
7 | APIEndpoints: {
8 | LOGIN: APIRoot + "/v1/login",
9 | REGISTRATION: APIRoot + "/v1/users",
10 | STORIES: APIRoot + "/v1/stories"
11 | },
12 |
13 | PayloadSources: keyMirror({
14 | SERVER_ACTION: null,
15 | VIEW_ACTION: null
16 | }),
17 |
18 | ActionTypes: keyMirror({
19 | // Session
20 | LOGIN_REQUEST: null,
21 | LOGIN_RESPONSE: null,
22 |
23 | // Routes
24 | REDIRECT: null,
25 |
26 | LOAD_STORIES: null,
27 | RECEIVE_STORIES: null,
28 | LOAD_STORY: null,
29 | RECEIVE_STORY: null,
30 | CREATE_STORY: null,
31 | RECEIVE_CREATED_STORY: null
32 | })
33 |
34 | };
35 |
--------------------------------------------------------------------------------
/scripts/dispatcher/SmallAppDispatcher.js:
--------------------------------------------------------------------------------
1 | var SmallConstants = require('../constants/SmallConstants.js');
2 | var Dispatcher = require('flux').Dispatcher;
3 | var assign = require('object-assign');
4 |
5 | var PayloadSources = SmallConstants.PayloadSources;
6 |
7 | var SmallAppDispatcher = assign(new Dispatcher(), {
8 |
9 | handleServerAction: function(action) {
10 | var payload = {
11 | source: PayloadSources.SERVER_ACTION,
12 | action: action
13 | };
14 | this.dispatch(payload);
15 | },
16 |
17 | handleViewAction: function(action) {
18 | var payload = {
19 | source: PayloadSources.VIEW_ACTION,
20 | action: action
21 | };
22 | this.dispatch(payload);
23 | }
24 | });
25 |
26 | module.exports = SmallAppDispatcher;
27 |
28 |
--------------------------------------------------------------------------------
/scripts/routes.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Router = require('react-router');
3 | var Route = Router.Route;
4 | var DefaultRoute = Router.DefaultRoute;
5 |
6 | var SmallApp = require('./components/SmallApp.react.jsx');
7 | var LoginPage = require('./components/session/LoginPage.react.jsx');
8 | var StoriesPage = require('./components/stories/StoriesPage.react.jsx');
9 | var StoryPage = require('./components/stories/StoryPage.react.jsx');
10 | var StoryNew = require('./components/stories/StoryNew.react.jsx');
11 | var SignupPage = require('./components/session/SignupPage.react.jsx');
12 |
13 | module.exports = (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 |
24 |
--------------------------------------------------------------------------------
/scripts/stores/RouteStore.react.jsx:
--------------------------------------------------------------------------------
1 | var SmallAppDispatcher = require('../dispatcher/SmallAppDispatcher.js');
2 | var SmallConstants = require('../constants/SmallConstants.js');
3 | var SessionStore = require('../stores/SessionStore.react.jsx');
4 | var StoryStore = require('../stores/StoryStore.react.jsx');
5 | var EventEmitter = require('events').EventEmitter;
6 | var assign = require('object-assign');
7 |
8 | var Router = require('react-router');
9 | var routes = require('../routes.jsx');
10 |
11 | var router = Router.create({
12 | routes: routes,
13 | location: null // Router.HistoryLocation
14 | });
15 |
16 | var ActionTypes = SmallConstants.ActionTypes;
17 | var CHANGE_EVENT = 'change';
18 |
19 | var RouteStore = assign({}, EventEmitter.prototype, {
20 |
21 | emitChange: function() {
22 | this.emit(CHANGE_EVENT);
23 | },
24 |
25 | addChangeListener: function(callback) {
26 | this.on(CHANGE_EVENT, callback);
27 | },
28 |
29 | removeChangeListener: function() {
30 | this.removeListener(CHANGE_EVENT, callback);
31 | },
32 |
33 | getRouter: function() {
34 | return router;
35 | },
36 |
37 | redirectHome: function() {
38 | router.transitionTo('app');
39 | }
40 | });
41 |
42 | RouteStore.dispatchToken = SmallAppDispatcher.register(function(payload) {
43 | SmallAppDispatcher.waitFor([
44 | SessionStore.dispatchToken,
45 | StoryStore.dispatchToken
46 | ]);
47 |
48 | var action = payload.action;
49 |
50 | switch(action.type) {
51 |
52 | case ActionTypes.REDIRECT:
53 | router.transitionTo(action.route);
54 | break;
55 |
56 | case ActionTypes.LOGIN_RESPONSE:
57 | if (SessionStore.isLoggedIn()) {
58 | router.transitionTo('app');
59 | // Dirty hack, need to figure this out
60 | $(document).foundation();
61 | }
62 | break;
63 |
64 | case ActionTypes.RECEIVE_CREATED_STORY:
65 | router.transitionTo('app');
66 | break;
67 |
68 | default:
69 | }
70 |
71 | return true;
72 | });
73 |
74 | module.exports = RouteStore;
75 |
76 |
--------------------------------------------------------------------------------
/scripts/stores/SessionStore.react.jsx:
--------------------------------------------------------------------------------
1 | var SmallAppDispatcher = require('../dispatcher/SmallAppDispatcher.js');
2 | var SmallConstants = require('../constants/SmallConstants.js');
3 | var EventEmitter = require('events').EventEmitter;
4 | var assign = require('object-assign');
5 |
6 | var ActionTypes = SmallConstants.ActionTypes;
7 | var CHANGE_EVENT = 'change';
8 |
9 | // Load an access token from the session storage, you might want to implement
10 | // a 'remember me' using localSgorage
11 | var _accessToken = sessionStorage.getItem('accessToken');
12 | var _email = sessionStorage.getItem('email');
13 | var _errors = [];
14 |
15 | var SessionStore = assign({}, EventEmitter.prototype, {
16 |
17 | emitChange: function() {
18 | this.emit(CHANGE_EVENT);
19 | },
20 |
21 | addChangeListener: function(callback) {
22 | this.on(CHANGE_EVENT, callback);
23 | },
24 |
25 | removeChangeListener: function(callback) {
26 | this.removeListener(CHANGE_EVENT, callback);
27 | },
28 |
29 | isLoggedIn: function() {
30 | return _accessToken ? true : false;
31 | },
32 |
33 | getAccessToken: function() {
34 | return _accessToken;
35 | },
36 |
37 | getEmail: function() {
38 | return _email;
39 | },
40 |
41 | getErrors: function() {
42 | return _errors;
43 | }
44 |
45 | });
46 |
47 | SessionStore.dispatchToken = SmallAppDispatcher.register(function(payload) {
48 | var action = payload.action;
49 |
50 | switch(action.type) {
51 |
52 | case ActionTypes.LOGIN_RESPONSE:
53 | if (action.json && action.json.access_token) {
54 | _accessToken = action.json.access_token;
55 | _email = action.json.email;
56 | // Token will always live in the session, so that the API can grab it with no hassle
57 | sessionStorage.setItem('accessToken', _accessToken);
58 | sessionStorage.setItem('email', _email);
59 | }
60 | if (action.errors) {
61 | _errors = action.errors;
62 | }
63 | SessionStore.emitChange();
64 | break;
65 |
66 | case ActionTypes.LOGOUT:
67 | _accessToken = null;
68 | _email = null;
69 | sessionStorage.removeItem('accessToken');
70 | sessionStorage.removeItem('email');
71 | SessionStore.emitChange();
72 | break;
73 |
74 | default:
75 | }
76 |
77 | return true;
78 | });
79 |
80 | module.exports = SessionStore;
81 |
82 |
--------------------------------------------------------------------------------
/scripts/stores/StoryStore.react.jsx:
--------------------------------------------------------------------------------
1 | var SmallAppDispatcher = require('../dispatcher/SmallAppDispatcher.js');
2 | var SmallConstants = require('../constants/SmallConstants.js');
3 | var EventEmitter = require('events').EventEmitter;
4 | var assign = require('object-assign');
5 | var WebAPIUtils = require('../utils/WebAPIUtils.js');
6 |
7 | var ActionTypes = SmallConstants.ActionTypes;
8 | var CHANGE_EVENT = 'change';
9 |
10 | var _stories = [];
11 | var _errors = [];
12 | var _story = { title: "", body: "", user: { username: "" } };
13 |
14 | var StoryStore = assign({}, EventEmitter.prototype, {
15 |
16 | emitChange: function() {
17 | this.emit(CHANGE_EVENT);
18 | },
19 |
20 | addChangeListener: function(callback) {
21 | this.on(CHANGE_EVENT, callback);
22 | },
23 |
24 | removeChangeListener: function(callback) {
25 | this.removeListener(CHANGE_EVENT, callback);
26 | },
27 |
28 | getAllStories: function() {
29 | return _stories;
30 | },
31 |
32 | getStory: function() {
33 | return _story;
34 | },
35 |
36 | getErrors: function() {
37 | return _errors;
38 | }
39 |
40 | });
41 |
42 | StoryStore.dispatchToken = SmallAppDispatcher.register(function(payload) {
43 | var action = payload.action;
44 |
45 | switch(action.type) {
46 |
47 | case ActionTypes.RECEIVE_STORIES:
48 | _stories = action.json.stories;
49 | StoryStore.emitChange();
50 | break;
51 |
52 | case ActionTypes.RECEIVE_CREATED_STORY:
53 | if (action.json) {
54 | _stories.unshift(action.json.story);
55 | _errors = [];
56 | }
57 | if (action.errors) {
58 | _errors = action.errors;
59 | }
60 | StoryStore.emitChange();
61 | break;
62 |
63 | case ActionTypes.RECEIVE_STORY:
64 | if (action.json) {
65 | _story = action.json.story;
66 | _errors = [];
67 | }
68 | if (action.errors) {
69 | _errors = action.errors;
70 | }
71 | StoryStore.emitChange();
72 | break;
73 | }
74 |
75 | return true;
76 | });
77 |
78 | module.exports = StoryStore;
79 |
80 |
--------------------------------------------------------------------------------
/scripts/utils/WebAPIUtils.js:
--------------------------------------------------------------------------------
1 | var ServerActionCreators = require('../actions/ServerActionCreators.react.jsx');
2 | var SmallConstants = require('../constants/SmallConstants.js');
3 | var request = require('superagent');
4 |
5 | function _getErrors(res) {
6 | var errorMsgs = ["Something went wrong, please try again"];
7 | if ((json = JSON.parse(res.text))) {
8 | if (json['errors']) {
9 | errorMsgs = json['errors'];
10 | } else if (json['error']) {
11 | errorMsgs = [json['error']];
12 | }
13 | }
14 | return errorMsgs;
15 | }
16 |
17 | var APIEndpoints = SmallConstants.APIEndpoints;
18 |
19 | module.exports = {
20 |
21 | signup: function(email, username, password, passwordConfirmation) {
22 | request.post(APIEndpoints.REGISTRATION)
23 | .send({ user: {
24 | email: email,
25 | username: username,
26 | password: password,
27 | password_confirmation: passwordConfirmation
28 | }})
29 | .set('Accept', 'application/json')
30 | .end(function(error, res) {
31 | if (res) {
32 | if (res.error) {
33 | var errorMsgs = _getErrors(res);
34 | ServerActionCreators.receiveLogin(null, errorMsgs);
35 | } else {
36 | json = JSON.parse(res.text);
37 | ServerActionCreators.receiveLogin(json, null);
38 | }
39 | }
40 | });
41 | },
42 |
43 | login: function(email, password) {
44 | request.post(APIEndpoints.LOGIN)
45 | .send({ email: email, password: password, grant_type: 'password' })
46 | .set('Accept', 'application/json')
47 | .end(function(error, res){
48 | if (res) {
49 | if (res.error) {
50 | var errorMsgs = _getErrors(res);
51 | ServerActionCreators.receiveLogin(null, errorMsgs);
52 | } else {
53 | json = JSON.parse(res.text);
54 | ServerActionCreators.receiveLogin(json, null);
55 | }
56 | }
57 | });
58 | },
59 |
60 | loadStories: function() {
61 | request.get(APIEndpoints.STORIES)
62 | .set('Accept', 'application/json')
63 | .set('Authorization', sessionStorage.getItem('accessToken'))
64 | .end(function(error, res){
65 | if (res) {
66 | json = JSON.parse(res.text);
67 | ServerActionCreators.receiveStories(json);
68 | }
69 | });
70 | },
71 |
72 | loadStory: function(storyId) {
73 | request.get(APIEndpoints.STORIES + '/' + storyId)
74 | .set('Accept', 'application/json')
75 | .set('Authorization', sessionStorage.getItem('accessToken'))
76 | .end(function(error, res){
77 | if (res) {
78 | json = JSON.parse(res.text);
79 | ServerActionCreators.receiveStory(json);
80 | }
81 | });
82 | },
83 |
84 | createStory: function(title, body) {
85 | request.post(APIEndpoints.STORIES)
86 | .set('Accept', 'application/json')
87 | .set('Authorization', sessionStorage.getItem('accessToken'))
88 | .send({ story: { title: title, body: body } })
89 | .end(function(error, res){
90 | if (res) {
91 | if (res.error) {
92 | var errorMsgs = _getErrors(res);
93 | ServerActionCreators.receiveCreatedStory(null, errorMsgs);
94 | } else {
95 | json = JSON.parse(res.text);
96 | ServerActionCreators.receiveCreatedStory(json, null);
97 | }
98 | }
99 | });
100 | }
101 |
102 | };
103 |
104 |
--------------------------------------------------------------------------------
/styles/fonts.scss:
--------------------------------------------------------------------------------
1 | /* Webfont: Lato-Thin */@font-face {
2 | font-family: 'LatoThin';
3 | src: url('../fonts/Lato-Thin.eot'); /* IE9 Compat Modes */
4 | src: url('../fonts/Lato-Thin.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
5 | url('../fonts/Lato-Thin.woff') format('woff'), /* Modern Browsers */
6 | url('../fonts/Lato-Thin.ttf') format('truetype');
7 | font-style: normal;
8 | font-weight: normal;
9 | text-rendering: optimizeLegibility;
10 | }
11 |
12 | /* Webfont: Lato-Light */@font-face {
13 | font-family: 'LatoLight';
14 | src: url('../fonts/Lato-Light.eot'); /* IE9 Compat Modes */
15 | src: url('../fonts/Lato-Light.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
16 | url('../fonts/Lato-Light.woff') format('woff'), /* Modern Browsers */
17 | url('../fonts/Lato-Light.ttf') format('truetype');
18 | font-style: normal;
19 | font-weight: normal;
20 | text-rendering: optimizeLegibility;
21 | }
22 |
23 | /* Webfont: Lato-Regular */@font-face {
24 | font-family: 'Lato';
25 | src: url('../fonts/Lato-Regular.eot'); /* IE9 Compat Modes */
26 | src: url('../fonts/Lato-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
27 | url('../fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
28 | url('../fonts/Lato-Regular.ttf') format('truetype');
29 | font-style: normal;
30 | font-weight: normal;
31 | text-rendering: optimizeLegibility;
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/styles/globals.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FancyPixel/small-frontend/8369b6088893b32d24bcef65c7ad5a8f69d2eda7/styles/globals.scss
--------------------------------------------------------------------------------
/styles/main.scss:
--------------------------------------------------------------------------------
1 | @import 'fonts';
2 | @import 'spinner';
3 | @import '../bower_components/foundation/scss/foundation/_functions.scss';
4 | @import url(http://fonts.googleapis.com/css?family=Raleway:700,400);
5 |
6 | $topbar-height: rem-calc(45);
7 |
8 | *, *:before, *:after {
9 | box-sizing: border-box;
10 | font-family: lato;
11 | }
12 |
13 | body {
14 | background-color: #ffffff;
15 | }
16 |
17 | .new-story {
18 | margin-top: 20px;
19 | }
20 |
21 | .new-story__title {
22 | input {
23 | border: none;
24 | box-shadow: none;
25 | border-bottom: 2px solid #ccc;
26 | }
27 | }
28 |
29 | .new-story__body {
30 | textarea {
31 | background-color: #fafafa;
32 | border: none;
33 | box-shadow: none;
34 | }
35 | }
36 | .new-story__submit {
37 |
38 | }
39 |
40 | .story {
41 | list-style: none;
42 | padding: 10px;
43 | margin-top: 10px;
44 | font-family: 'Raleway', sans-serif;
45 | }
46 |
47 | .story__title {
48 | text-decoration: none;
49 | font-weight: 700;
50 | font-size: rem-calc(24);
51 | a {
52 | color: black;
53 | }
54 | }
55 |
56 | .story__body {
57 | font-weight: 400;
58 | font-size: rem-calc(14);
59 | color: #333;
60 | font-style: italic;
61 | }
62 |
63 | .story__user {
64 | font-weight: 400;
65 | font-size: rem-calc(12);
66 | color: #2E8BA9;
67 | }
68 |
69 | .story__date {
70 | color: #aaa;
71 | font-size: rem-calc(12);
72 | }
73 |
74 | .card {
75 | margin-bottom: 1em;
76 | padding: 20px;
77 | background-color: #ECF0F1;
78 | border-bottom: 4px solid #D4D7D8;
79 | ul {
80 | margin-bottom: 0;
81 | }
82 | }
83 |
84 | .card--login {
85 | margin-top: 20px;
86 | }
87 |
88 | .card--login__field {
89 | label {
90 | font-family: lato-light;
91 | }
92 | input {
93 | border-width: 0px;
94 | border-left: 4px solid #c2c2c2;
95 | box-shadow: none;
96 | }
97 | }
98 |
99 | .card--login__submit {
100 | width: 100%;
101 | margin: 0;
102 | margin-top: 10px;
103 | font-family: 'Lato'
104 | }
105 |
106 | .error-notice {
107 | position: absolute;
108 | top: $topbar-height;
109 | width: 100%;
110 | z-index: 9999;
111 | background-color: #E44646;
112 | color: white;
113 | text-align: center;
114 | padding: 5px;
115 | ul {
116 | list-style: none;
117 | margin: 0;
118 | }
119 | }
120 |
121 |
--------------------------------------------------------------------------------
/styles/spinner.scss:
--------------------------------------------------------------------------------
1 | .spinner {
2 | margin: 10px auto 0;
3 | width: 70px;
4 | text-align: center;
5 | }
6 |
7 | .spinner > div {
8 | width: 18px;
9 | height: 18px;
10 | background-color: #3499BB;
11 |
12 | border-radius: 100%;
13 | display: inline-block;
14 | -webkit-animation: bouncedelay 1.4s infinite ease-in-out;
15 | animation: bouncedelay 1.4s infinite ease-in-out;
16 | /* Prevent first frame from flickering when animation starts */
17 | -webkit-animation-fill-mode: both;
18 | animation-fill-mode: both;
19 | }
20 |
21 | .spinner .bounce1 {
22 | -webkit-animation-delay: -0.32s;
23 | animation-delay: -0.32s;
24 | }
25 |
26 | .spinner .bounce2 {
27 | -webkit-animation-delay: -0.16s;
28 | animation-delay: -0.16s;
29 | }
30 |
31 | @-webkit-keyframes bouncedelay {
32 | 0%, 80%, 100% { -webkit-transform: scale(0.0) }
33 | 40% { -webkit-transform: scale(1.0) }
34 | }
35 |
36 | @keyframes bouncedelay {
37 | 0%, 80%, 100% {
38 | transform: scale(0.0);
39 | -webkit-transform: scale(0.0);
40 | } 40% {
41 | transform: scale(1.0);
42 | -webkit-transform: scale(1.0);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------