├── .gitignore ├── README.md ├── gulpfile.babel.js ├── package.json ├── src ├── actors │ ├── AnimatorActor.js │ └── UserInterfaceActor.jsx ├── components │ ├── AboutPage │ │ ├── AboutPage.jsx │ │ └── AboutPage.less │ ├── Application.jsx │ ├── ApplicationLayout │ │ ├── ApplicationLayout.jsx │ │ └── ApplicationLayout.less │ ├── Base.jsx │ ├── BlackTrianglePage │ │ ├── BlackTrianglePage.jsx │ │ └── BlackTrianglePage.less │ ├── Link.jsx │ ├── NavMenu │ │ ├── NavMenu.jsx │ │ └── NavMenu.less │ └── NotFoundPage │ │ ├── NotFoundPage.js │ │ └── NotFoundPage.less ├── controls │ ├── BlackTriangleControl.js │ └── NavigationControl.js ├── index.html ├── main.js ├── models │ ├── BlackTriangleModel.js │ └── NavigationModel.js ├── static │ └── .gitkeep ├── theme │ └── theme.less └── utils │ └── router.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | dist-intermediate 5 | *.log 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-black-triangle 2 | 3 | *HELLO: react-black-triangle (and [Maxim](https://github.com/jamesknelson/maxim)) are at v0.1 - if you find this neat, you can help make it even better by letting me know where you'd like to see more documentation!* 4 | 5 | **react-black-triangle provides you with the code and conventions you need to get straight into building your React-based app.** 6 | 7 | ## Happiness is six lines away 8 | 9 | *Prerequisites: node.js and git* 10 | 11 | ``` 12 | git clone git@github.com:jamesknelson/react-black-triangle.git [your-repo-name] 13 | cd [your-repo-name] 14 | npm install 15 | npm install -g gulp 16 | npm start 17 | npm run open # (from a different console window, otherwise open localhost:3000) 18 | ``` 19 | 20 | Presto, Black Triangle! 21 | 22 | ![react-black-triangle](http://jamesknelson.com/black-triangle.png) 23 | 24 | ## Why use react-black-triangle? 25 | 26 | - Your directory structure is sorted as soon as you `git clone` 27 | - ES6 compilation and automatic-reloading development server are all handled by `npm start` 28 | - [Maxim](https://github.com/jamesknelson/maxim) (based on [RxJS](https://github.com/Reactive-Extensions/RxJS)) makes your data flow easy to reason about 29 | - CSS conventions and helper functions *completely eliminate* bugs caused by conflicting styles 30 | - Elegant routing is included *without* depending on the confusing `react-router` 31 | - A simple layout is included to help you get started on the important stuff right away 32 | - Comes with a cool spinning [Black Triangle](http://rampantgames.com/blog/?p=7745) - *how fucking awesome is that?!* 33 | 34 | ## Getting Started 35 | 36 | Put your name on it: 37 | 38 | - Update name and author in package.json 39 | - Update app title in `src/index.html` 40 | - Restart the dev server (make sure to do this after any changes to `src/index.html`) 41 | 42 | Make sure your editor is happy 43 | 44 | - Setup ES6 syntax highlighting on extensions `.js` and `.jsx` (see [babel-sublime](https://github.com/babel/babel-sublime)) 45 | 46 | Remove the stupid black triangle and any references to it: 47 | 48 | - `BlackTriangleControl` 49 | - `BlackTriangleModel` 50 | - `AnimatorActor` 51 | - `BlackTrianglePage` 52 | 53 | Start building! 54 | 55 | - Add a route to `src/utils/router.js` 56 | - Add a nav menu item in `src/components/NavMenu/NavMenu.jsx` 57 | - Add a component for it in `src/components` 58 | - Add the new component and route to `src/components/Application.jsx` and `src/theme/theme.less` 59 | - Bask in the glory of your creation 60 | - Don't forget to commit your changes and push to bitbucket or github! 61 | 62 | Show your friends 63 | 64 | - Run `gulp dist` to output a web-ready build of your app to `dist` 65 | 66 | ## Structure 67 | 68 | ### Model 69 | 70 | Your data - including routes, authentication, view model data, and anything else you can imagine - is managed by [Maxim](https://github.com/jamesknelson/maxim). 71 | 72 | Maxim is similar to Facebook's [Flux](https://facebook.github.io/flux/), in that data flow is unidirectional. Events start at your Controls, then flow through your Models, Reducers, Actors, and finally through your Components to the user's screen. 73 | 74 | Maxim gives you four classes with well defined responsibilities: 75 | 76 | #### Controls 77 | 78 | All possible actions which can affect the state of your model are defined in these Controls. 79 | 80 | Each `control` file defines a number of action functions. Action functions can do things which change the outside world (like making HTTP requests or setting timers), and can call other actions in the same control module (as long as the calls are in a separate tick). 81 | 82 | Once an action has run any necessary code, it should call `this` to emit an event to be processed by model modules. 83 | 84 | #### Models 85 | 86 | Models take the [Rx.Observable](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md) objects from the above action files as inputs. Each model returns a single observable itself, which represents the state of that model. 87 | 88 | #### Reducers 89 | 90 | Reducers take the Rx.Observable objects produced from models and other reducers, and further process them into more Rx.Observables. 91 | 92 | #### Actors 93 | 94 | Actors subscribe to the Rx.Observable objects from your Model and Reducer files, using the result to change the outside world through actions such as displaying a UI or calling further control actions. 95 | 96 | ### View 97 | 98 | Of course, having an actor which is called with the latest version of your data is only half the problem - you need to effectively communicate this data to your users. We accomplish this with (surprise!) [React](https://facebook.github.io/react/)! 99 | 100 | To help concenctrate on actually building your application, use these conventions when building your user interface: 101 | 102 | #### Structure 103 | 104 | - `UserInterfaceActor` renders the `Application` component every time any model/reducer data changes 105 | - Components should access control actions through their `this.context.Actions` property (see `BlackTrianglePage.jsx` for an example) 106 | - Data should be passed as props, and *not* passed through `context`. 107 | - Make sure each of your components have their own directory under `src/components` (other than the special `Application`, `Base` and `Link` components). 108 | - Each component should inherit from the Base component (which provides helpers for genrating classes, see `src/components/Base.jsx` for documentation) 109 | - Control Actions are 110 | 111 | #### Style rules 112 | 113 | - Each component's root element should have a class named after the component itself 114 | - Each component should accept a `className` prop which adds any specified classes to it's own classes 115 | - Style selectors should always be a single class, and optionally a pseudo-selector. For example: 116 | 117 | * `.NavMenu .Link` is bad 118 | * `.NavMenu-item` is good 119 | 120 | The `this.c` method on every component which inherits from base helps with making these methods. 121 | 122 | ### Summary 123 | 124 | Maxim directories: 125 | 126 | - `src/controls` - Each `control` contains actions which can be called by actors 127 | - `src/models` - Each `model` maintains a single value, updating it based on control events 128 | - `src/reducers` - Further process the values of models and other reducers 129 | - `src/actors` - Interfact with the world based on changes to your model 130 | 131 | UI directories: 132 | 133 | - `src/components` - React components and their associated stylesheets 134 | - `src/theme` - Global CSS (you generally don't put anything here except imports for component stylesheets) 135 | 136 | Other directories: 137 | 138 | - `build` - Intermediate files produced by the development server. Don't touch these. 139 | - `src/utils` - Pure functions which you may want to use across your entire project go here 140 | - `src/static` - Files which will be copied across to the root directory on build 141 | 142 | Individual modules (documentation coming soon): 143 | 144 | - `src/actors/UserInterfaceActor.jsx` 145 | - `src/components/Application.jsx` 146 | - `src/components/Base.jsx` 147 | - `src/components/Link.jsx` 148 | - `src/controls/NavigationControl.js` 149 | - `src/models/NavigationModel.js` 150 | - `src/theme/theme.less` 151 | - `src/utils/router.js` 152 | - `src/index.html` 153 | - `src/main.js` 154 | 155 | And other files: 156 | 157 | - `gulpfile.babel.js` - Build scripts written with [gulp](http://gulpjs.com/) 158 | - `webpack.config.js` - [Webpack](http://webpack.github.io/) configuration 159 | 160 | ## TODO 161 | 162 | - Watch `static` and `index.html` for changes and copy them across to `build` when appropriate 163 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import del from "del"; 2 | import path from "path"; 3 | import gulp from "gulp"; 4 | import open from "open"; 5 | import gulpLoadPlugins from "gulp-load-plugins"; 6 | import packageJson from "./package.json"; 7 | import runSequence from "run-sequence"; 8 | import webpack from "webpack"; 9 | import webpackConfig from "./webpack.config"; 10 | import WebpackDevServer from "webpack-dev-server"; 11 | 12 | 13 | const PORT = process.env.PORT || 3000; 14 | const $ = gulpLoadPlugins({camelize: true}); 15 | 16 | 17 | // Main tasks 18 | gulp.task('serve', () => runSequence('serve:clean', 'serve:index', 'serve:start')); 19 | gulp.task('dist', () => runSequence('dist:clean', 'dist:build', 'dist:index')); 20 | gulp.task('clean', ['dist:clean', 'serve:clean']); 21 | gulp.task('open', () => open('http://localhost:3000')); 22 | 23 | // Remove all built files 24 | gulp.task('serve:clean', cb => del('build', {dot: true}, cb)); 25 | gulp.task('dist:clean', cb => del(['dist', 'dist-intermediate'], {dot: true}, cb)); 26 | 27 | // Copy static files across to our final directory 28 | gulp.task('serve:static', () => 29 | gulp.src([ 30 | 'src/static/**' 31 | ]) 32 | .pipe($.changed('build')) 33 | .pipe(gulp.dest('build')) 34 | .pipe($.size({title: 'static'})) 35 | ); 36 | 37 | gulp.task('dist:static', () => 38 | gulp.src([ 39 | 'src/static/**' 40 | ]) 41 | .pipe(gulp.dest('dist')) 42 | .pipe($.size({title: 'static'})) 43 | ); 44 | 45 | // Copy our index file and inject css/script imports for this build 46 | gulp.task('serve:index', () => { 47 | return gulp 48 | .src('src/index.html') 49 | .pipe($.injectString.after('', '')) 50 | .pipe(gulp.dest('build')); 51 | }); 52 | 53 | // Copy our index file and inject css/script imports for this build 54 | gulp.task('dist:index', () => { 55 | const app = gulp 56 | .src(["*.{css,js}"], {cwd: 'dist-intermediate/generated'}) 57 | .pipe(gulp.dest('dist')); 58 | 59 | // Build the index.html using the names of compiled files 60 | return gulp.src('src/index.html') 61 | .pipe($.inject(app, { 62 | ignorePath: 'dist', 63 | starttag: '' 64 | })) 65 | .on("error", $.util.log) 66 | .pipe(gulp.dest('dist')); 67 | }); 68 | 69 | // Start a livereloading development server 70 | gulp.task('serve:start', ['serve:static'], () => { 71 | const config = webpackConfig(true, 'build', PORT); 72 | 73 | return new WebpackDevServer(webpack(config), { 74 | contentBase: 'build', 75 | publicPath: config.output.publicPath, 76 | hot: true, 77 | watchDelay: 100 78 | }) 79 | .listen(PORT, '0.0.0.0', (err) => { 80 | if (err) throw new $.util.PluginError('webpack-dev-server', err); 81 | 82 | $.util.log(`[${packageJson.name} serve]`, `Listening at 0.0.0.0:${PORT}`); 83 | }); 84 | }); 85 | 86 | // Create a distributable package 87 | gulp.task('dist:build', ['dist:static'], cb => { 88 | const config = webpackConfig(false, 'dist-intermediate'); 89 | 90 | webpack(config, (err, stats) => { 91 | if (err) throw new $.util.PluginError('dist', err); 92 | 93 | $.util.log(`[${packageJson.name} dist]`, stats.toString({colors: true})); 94 | 95 | cb(); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-black-triangle", 3 | "description": "", 4 | "version": "1.0.0", 5 | "author": "James K Nelson ", 6 | "license": "MIT", 7 | "private": true, 8 | "main": "static", 9 | "scripts": { 10 | "start": "gulp serve", 11 | "open": "gulp open", 12 | "test": "jest" 13 | }, 14 | "devDependencies": { 15 | "autoprefixer-loader": "^2.0.0", 16 | "babel": "^5.5.5", 17 | "babel-core": "^5.5.5", 18 | "babel-loader": "^5.1.4", 19 | "css-loader": "^0.14.4", 20 | "del": "^1.2.0", 21 | "extract-text-webpack-plugin": "^0.8.1", 22 | "fill-range": "^2.2.2", 23 | "gulp": "^3.9.0", 24 | "gulp-changed": "^1.2.1", 25 | "gulp-inject": "^1.3.1", 26 | "gulp-inject-string": "0.0.2", 27 | "gulp-load-plugins": "^0.10.0", 28 | "gulp-size": "^1.2.1", 29 | "gulp-util": "^3.0.5", 30 | "less": "^2.5.1", 31 | "less-loader": "^2.2.0", 32 | "node-libs-browser": "^0.5.2", 33 | "open": "0.0.5", 34 | "run-sequence": "^1.1.0", 35 | "style-loader": "^0.12.3", 36 | "webpack": "^1.9.10", 37 | "webpack-dev-server": "^1.9.0" 38 | }, 39 | "dependencies": { 40 | "classnames": "^2.1.2", 41 | "deep-equal": "^1.0.0", 42 | "fastclick": "^1.0.6", 43 | "maxim": "^0.2.0", 44 | "react": "^0.13.3", 45 | "routr": "^0.1.2", 46 | "rx": "^2.5.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/actors/AnimatorActor.js: -------------------------------------------------------------------------------- 1 | export default function AnimatorActor(Actions, Replayables) { 2 | let lastTime = new Date().getTime() / 1000; 3 | 4 | Replayables.BlackTriangle.delay(20).subscribe(({speed}) => { 5 | const time = new Date().getTime() / 1000; 6 | const delta = time - lastTime; 7 | lastTime = time; 8 | 9 | Actions.BlackTriangle.rotate(delta * speed); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/actors/UserInterfaceActor.jsx: -------------------------------------------------------------------------------- 1 | import Rx from "rx"; 2 | import React from "react"; 3 | import FastClick from "fastclick"; 4 | import Application from "../components/Application"; 5 | 6 | 7 | React.initializeTouchEvents(true); 8 | FastClick.attach(document.body); 9 | 10 | 11 | export default function UserInterfaceActor(Actions, Replayables) { 12 | Rx.Observable.combineLatest( 13 | Replayables.Navigation, 14 | Replayables.BlackTriangle, 15 | 16 | (route, triangle) => ({route, triangle}) 17 | ) 18 | .subscribe(state => { 19 | React.render( 20 | , 21 | document.getElementById('react-app') 22 | ) 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/AboutPage/AboutPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Base from "../Base"; 3 | 4 | export default class AboutPage extends Base { 5 | render() { 6 | return ( 7 |
8 |

About

9 |
10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/AboutPage/AboutPage.less: -------------------------------------------------------------------------------- 1 | .AboutPage { 2 | margin: 16px 16px 16px 80px 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Application.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Base from "./Base"; 3 | import ApplicationLayout from "./ApplicationLayout/ApplicationLayout"; 4 | import NavMenu from "./NavMenu/NavMenu"; 5 | import BlackTrianglePage from "./BlackTrianglePage/BlackTrianglePage"; 6 | import AboutPage from "./AboutPage/AboutPage"; 7 | import NotFoundPage from "./NotFoundPage/NotFoundPage"; 8 | 9 | 10 | class Application extends Base { 11 | getChildContext() { 12 | return { 13 | Actions: this.props.Actions, 14 | currentRoute: this.props.route 15 | }; 16 | } 17 | 18 | render() { 19 | let page; 20 | 21 | // 22 | // Create a component based on the current route 23 | // 24 | 25 | switch (this.props.route.name) { 26 | case "about": 27 | page = ; 28 | break; 29 | 30 | case "triangle": 31 | page = ; 32 | break; 33 | 34 | default: 35 | page = 36 | } 37 | 38 | return ( 39 | } 43 | /> 44 | ); 45 | } 46 | } 47 | 48 | Application.childContextTypes = { 49 | Actions: React.PropTypes.object, 50 | currentRoute: React.PropTypes.object 51 | }; 52 | 53 | export default Application; 54 | -------------------------------------------------------------------------------- /src/components/ApplicationLayout/ApplicationLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Base from "../Base"; 3 | 4 | export default class ApplicationLayout extends Base { 5 | render() { 6 | return ( 7 |
8 |
9 | {this.props.nav} 10 |
11 |
12 | {this.props.page} 13 |
14 |
15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/ApplicationLayout/ApplicationLayout.less: -------------------------------------------------------------------------------- 1 | .ApplicationLayout { 2 | height: 100%; 3 | 4 | &-nav { 5 | position: fixed; 6 | 7 | width: 256px; 8 | left: 0; 9 | top: 0; 10 | bottom: 0; 11 | } 12 | 13 | &-page { 14 | margin-left: 256px; 15 | height: 100%; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Base.jsx: -------------------------------------------------------------------------------- 1 | import {Component, PropTypes} from "react"; 2 | import classNames from "classnames"; 3 | 4 | 5 | class Base extends Component { 6 | getComponentClasses(classNames, ...overrideClassNames) { 7 | return [ 8 | this.constructor.name, 9 | this.c(classNames), 10 | this.props.className || "", 11 | this.c(overrideClassNames) 12 | ].join(' '); 13 | } 14 | 15 | c(...args) { 16 | return ( 17 | classNames(...args) 18 | .split(/\s+/) 19 | .filter(name => name !== "") 20 | .map(name => `${this.constructor.name}-${name}`) 21 | .join(' ') 22 | ); 23 | } 24 | } 25 | 26 | Base.propTypes = { 27 | className: PropTypes.string 28 | }; 29 | 30 | 31 | export default Base; 32 | -------------------------------------------------------------------------------- /src/components/BlackTrianglePage/BlackTrianglePage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Base from "../Base"; 3 | 4 | class BlackTriangle extends Base { 5 | render() { 6 | const adjustSpeed = this.context.Actions.BlackTriangle.adjustSpeed; 7 | 8 | return ( 9 |
10 |

Use buttons to change rotation speed

11 |
15 | 19 | 22 |
23 | ); 24 | } 25 | } 26 | 27 | BlackTriangle.contextTypes = { 28 | Actions: React.PropTypes.object 29 | }; 30 | 31 | export default BlackTriangle; 32 | -------------------------------------------------------------------------------- /src/components/BlackTrianglePage/BlackTrianglePage.less: -------------------------------------------------------------------------------- 1 | .BlackTriangle { 2 | position: relative; 3 | height: 100%; 4 | 5 | h1 { 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | margin-top: -230px; 10 | text-align: center; 11 | width: 600px; 12 | margin-left: -300px; 13 | 14 | font-size: 18px; 15 | text-transform: uppercase; 16 | font-weight: normal; 17 | letter-spacing: 3px; 18 | } 19 | 20 | &-inner { 21 | position:absolute; 22 | top:50%; 23 | left:50%; 24 | width:277.2px; 25 | height:277.2px; 26 | margin-top:-138.6px; 27 | margin-left:-138.6px; 28 | border-radius:100%; 29 | 30 | &:before { 31 | position:absolute; 32 | z-index:1; 33 | top:50%; 34 | left:50%; 35 | margin-top:-136px; 36 | margin-left:-120px; 37 | content:""; 38 | width: 0; 39 | height: 0; 40 | border-style: solid; 41 | border-width: 0 120px 207.8px 120px; 42 | border-color: transparent transparent #000 transparent; 43 | } 44 | } 45 | 46 | &-left, &-right { 47 | position: absolute; 48 | top: 50%; 49 | left: 50%; 50 | margin-top: -24px; 51 | font-size: 24px; 52 | background: black; 53 | color: white; 54 | border-radius: 50%; 55 | height: 48px; 56 | width: 48px; 57 | border: none; 58 | outline: 0; 59 | cursor: pointer; 60 | transition: background-color ease-out 0.15s, color ease-out 0.15s; 61 | 62 | &:hover { 63 | color: black; 64 | background-color: white; 65 | } 66 | } 67 | 68 | &-left { 69 | margin-left: -278px; 70 | } 71 | 72 | &-right { 73 | margin-left: 230px; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/Link.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import equal from "deep-equal"; 3 | import Base from "./Base"; 4 | import router from "../utils/router"; 5 | 6 | 7 | class Link extends Base { 8 | isActive() { 9 | const stateParts = this.context.currentRoute.name.split('.'); 10 | const propsParts = this.props.to.split('.'); 11 | 12 | return ( 13 | equal(stateParts.slice(0, propsParts.length), propsParts) && 14 | (!this.props.params || equal(this.context.currentRoute.params, this.props.params)) 15 | ); 16 | } 17 | 18 | render() { 19 | const activeClass = this.isActive() ? ' '+this.props.activeClassName : ''; 20 | 21 | return ( 22 | 26 | {this.props.children} 27 | 28 | ); 29 | } 30 | } 31 | 32 | Link.contextTypes = { 33 | Actions: React.PropTypes.object, 34 | currentRoute: React.PropTypes.object 35 | }; 36 | 37 | Link.propTypes = { 38 | activeClassName: React.PropTypes.string.isRequired, 39 | to: React.PropTypes.string.isRequired, 40 | params: React.PropTypes.object 41 | }; 42 | 43 | Link.defaultProps = { 44 | activeClassName: "Link-active" 45 | }; 46 | 47 | export default Link; 48 | -------------------------------------------------------------------------------- /src/components/NavMenu/NavMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Base from "../Base"; 3 | import Link from "../Link"; 4 | 5 | 6 | class NavMenuItem extends Base { 7 | render() { 8 | return ( 9 | 13 | 14 | {this.props.title} 15 | 16 | ) 17 | } 18 | } 19 | 20 | 21 | export default class NavMenu extends Base { 22 | render() { 23 | const linkActiveClass = this.c("active"); 24 | 25 | return ( 26 | 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/NavMenu/NavMenu.less: -------------------------------------------------------------------------------- 1 | .NavMenu { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | width: 100%; 6 | 7 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24); 8 | 9 | &-inner { 10 | box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12); 11 | } 12 | } 13 | 14 | .NavMenuItem { 15 | display: block; 16 | height: 64px; 17 | color: black; 18 | text-decoration: none; 19 | background-color: #ffffff; 20 | transition: color ease-out 0.15s, background-color ease-out 0.15s; 21 | 22 | &-icon { 23 | font-size: 24px; 24 | width: 24px; 25 | height: 24px; 26 | text-align: center; 27 | padding: 8px; 28 | margin: 12px 24px 12px 16px; 29 | } 30 | 31 | &-title { 32 | display: inline-block; 33 | margin: 12px 0; 34 | line-height: 40px; 35 | vertical-align: top; 36 | } 37 | 38 | &:hover { 39 | color: #2985e0; 40 | background-color: lighten(#2985e0, 47%); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/NotFoundPage/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Base from "../Base"; 3 | 4 | export default class NotFoundPage extends Base { 5 | render() { 6 | return ( 7 |
8 |

404

9 |
10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/NotFoundPage/NotFoundPage.less: -------------------------------------------------------------------------------- 1 | .NotFoundPage { 2 | position: relative; 3 | height: 100%; 4 | 5 | &-title { 6 | position: absolute; 7 | text-align: center; 8 | top: 50%; 9 | left: 50%; 10 | width: 400px; 11 | margin-left: -200px; 12 | margin-top: -100px; 13 | font-size: 100px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/controls/BlackTriangleControl.js: -------------------------------------------------------------------------------- 1 | export default (Actions, Replayables) => ({ 2 | initialize() { 3 | this({speed: 90, angle: 0}); 4 | }, 5 | 6 | adjustSpeed(speed) { 7 | this({speed}); 8 | }, 9 | 10 | rotate(angle) { 11 | this({angle}); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/controls/NavigationControl.js: -------------------------------------------------------------------------------- 1 | import router from "../utils/router"; 2 | 3 | function getLocation() { 4 | return window.location.hash.substr(1); 5 | } 6 | 7 | function getRoute() { 8 | return router.getRoute(getLocation()) || {name: '404'}; 9 | } 10 | 11 | // Make sure that there is a slash immediately following '#' 12 | function ensureSlash() { 13 | const path = getLocation(); 14 | 15 | if (path.charAt(0) == '/') { 16 | return true; 17 | } 18 | else { 19 | const windowPath = window.location.pathname + window.location.search; 20 | window.location.replace(windowPath + '#/' + path); 21 | return false; 22 | } 23 | } 24 | 25 | export default (Actions, Replayables) => ({ 26 | initialize() { 27 | // If we don't need to redirect because of a missing slash or some other reason, 28 | // emit a navigatedTo event. 29 | function onHashChange() { 30 | ensureSlash(); 31 | Actions.Navigation.navigatedTo(getRoute()); 32 | } 33 | 34 | window.addEventListener('hashchange', onHashChange, false); 35 | ensureSlash(); 36 | this(getRoute()); 37 | }, 38 | 39 | // Triggered when the hash is updated, either by a link or by manual navigation 40 | navigatedTo(route) { 41 | this(route); 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Black Triangle 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import {initialize} from "maxim"; 2 | 3 | // Controls 4 | import NavigationControl from "./controls/NavigationControl"; 5 | import BlackTriangleControl from "./controls/BlackTriangleControl"; 6 | 7 | // Models 8 | import NavigationModel from "./models/NavigationModel"; 9 | import BlackTriangleModel from "./models/BlackTriangleModel"; 10 | 11 | // Actors 12 | import UserInterfaceActor from "./actors/UserInterfaceActor"; 13 | import AnimatorActor from "./actors/AnimatorActor"; 14 | 15 | 16 | const app = initialize({ 17 | controls: { 18 | Navigation: NavigationControl, 19 | BlackTriangle: BlackTriangleControl, 20 | }, 21 | models: { 22 | Navigation: NavigationModel, 23 | BlackTriangle: BlackTriangleModel, 24 | }, 25 | actors: [ 26 | UserInterfaceActor, 27 | AnimatorActor 28 | ], 29 | }); 30 | -------------------------------------------------------------------------------- /src/models/BlackTriangleModel.js: -------------------------------------------------------------------------------- 1 | import Rx from "rx"; 2 | 3 | 4 | export default function BlackTriangleModel(Observables) { 5 | return Rx.Observable.concat( 6 | Observables.BlackTriangle.initialize, 7 | Rx.Observable.merge( 8 | Observables.BlackTriangle.rotate, 9 | Observables.BlackTriangle.adjustSpeed 10 | ) 11 | ) 12 | .scan((current, delta) => { 13 | const {angle: currentAngle, speed: currentSpeed} = current; 14 | const {angle: angleDelta, speed: speedDelta} = delta; 15 | 16 | const angle = (currentAngle + (angleDelta || 0)) % 360; 17 | const speed = currentSpeed + (speedDelta || 0); 18 | 19 | return {angle, speed}; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/models/NavigationModel.js: -------------------------------------------------------------------------------- 1 | import Rx from "rx"; 2 | 3 | 4 | export default function NavigationModel(Observables) { 5 | return Rx.Observable.concat( 6 | Observables.Navigation.initialize, 7 | Observables.Navigation.navigatedTo 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/react-black-triangle/c9c0797099f0319a79026abc7badce968d187335/src/static/.gitkeep -------------------------------------------------------------------------------- /src/theme/theme.less: -------------------------------------------------------------------------------- 1 | html, body, #react-app { 2 | height: 100%; 3 | min-height: 100%; 4 | margin: 0; 5 | font-family: Roboto; 6 | } 7 | 8 | // `.less` extensions must be added to placate `extract-text-webpack-plugin` 9 | @import "../components/AboutPage/AboutPage.less"; 10 | @import "../components/ApplicationLayout/ApplicationLayout.less"; 11 | @import "../components/BlackTrianglePage/BlackTrianglePage.less"; 12 | @import "../components/NavMenu/NavMenu.less"; 13 | @import "../components/NotFoundPage/NotFoundPage.less"; 14 | -------------------------------------------------------------------------------- /src/utils/router.js: -------------------------------------------------------------------------------- 1 | import Routr from "routr" 2 | 3 | const router = new Routr({ 4 | 'triangle': {path: "/", method: "get"}, 5 | 'about': {path: "/about", method: "get"}, 6 | }); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import webpack from "webpack"; 3 | import ExtractTextPlugin from "extract-text-webpack-plugin"; 4 | 5 | 6 | export default (DEBUG, PATH, PORT=3000) => ({ 7 | entry: (DEBUG ? [ 8 | `webpack-dev-server/client?http://localhost:${PORT}`, 9 | 'webpack/hot/dev-server' 10 | ] : []).concat([ 11 | './src/theme/theme.less', 12 | 'babel/polyfill', 13 | './src/main' 14 | ]), 15 | 16 | output: { 17 | path: path.resolve(__dirname, PATH, "generated"), 18 | filename: DEBUG ? "main.js" : "main-[hash].js", 19 | publicPath: "/generated/" 20 | }, 21 | 22 | cache: DEBUG, 23 | debug: DEBUG, 24 | 25 | // For options, see http://webpack.github.io/docs/configuration.html#devtool 26 | devtool: DEBUG && "eval-source-map", 27 | 28 | module: { 29 | loaders: [ 30 | // Load ES6/JSX 31 | { test: /\.jsx?$/, include: path.join(__dirname, "src"), loader: "babel-loader" }, 32 | 33 | // Load styles 34 | { test: /\.less$/, 35 | loader: DEBUG 36 | ? "style!css!autoprefixer!less" 37 | : ExtractTextPlugin.extract("style-loader", "css-loader!autoprefixer-loader!less-loader") }, 38 | 39 | // Load images 40 | { test: /\.jpg/, loader: "url-loader?limit=10000&mimetype=image/jpg" }, 41 | { test: /\.gif/, loader: "url-loader?limit=10000&mimetype=image/gif" }, 42 | { test: /\.png/, loader: "url-loader?limit=10000&mimetype=image/png" }, 43 | { test: /\.svg/, loader: "url-loader?limit=10000&mimetype=image/svg" }, 44 | 45 | // Load fonts 46 | { test: /\.woff$/, loader: "url-loader?limit=10000&minetype=application/font-woff" }, 47 | { test: /\.(ttf|eot|svg)$/, loader: "file-loader" } 48 | ] 49 | }, 50 | 51 | plugins: DEBUG 52 | ? [ 53 | new webpack.HotModuleReplacementPlugin(), 54 | new webpack.NoErrorsPlugin() 55 | ] 56 | : [ 57 | new webpack.DefinePlugin({'process.env.NODE_ENV': '"production"'}), 58 | new ExtractTextPlugin("style.css", {allChunks: false}), 59 | new webpack.optimize.DedupePlugin(), 60 | new webpack.optimize.UglifyJsPlugin({ 61 | compressor: {screw_ie8: true, keep_fnames: true, warnings: false}, 62 | mangle: {screw_ie8: true, keep_fnames: true} 63 | }), 64 | new webpack.optimize.OccurenceOrderPlugin(), 65 | new webpack.optimize.AggressiveMergingPlugin() 66 | ], 67 | 68 | resolve: { 69 | modulesDirectories: [ 70 | "node_modules", 71 | 72 | // https://github.com/webpack/webpack-dev-server/issues/60 73 | "web_modules" 74 | ], 75 | 76 | // Allow to omit extensions when requiring these files 77 | extensions: ["", ".js", ".jsx", ".es6"] 78 | } 79 | }); 80 | --------------------------------------------------------------------------------