├── src ├── layout │ ├── Layout.css │ ├── Layout.js │ └── NavBar.js ├── components │ ├── spinner.gif │ ├── Spinner.css │ ├── Spinner.js │ ├── Button.js │ ├── Markdown.js │ ├── NewPageModal.js │ ├── Modal.js │ └── ContentBlock.js ├── main.js ├── pages │ ├── AboutPage.js │ ├── ContentPage.js │ └── HomePage.js └── data │ └── Content.js ├── .gitignore ├── webpack.config.js ├── package.json ├── index.html └── README.md /src/layout/Layout.css: -------------------------------------------------------------------------------- 1 | .content { 2 | padding-top: 55px; 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *~ 3 | build/main.js 4 | build/main.js.map 5 | .DS_Store -------------------------------------------------------------------------------- /src/components/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petehunt/ReactHack/HEAD/src/components/spinner.gif -------------------------------------------------------------------------------- /src/components/Spinner.css: -------------------------------------------------------------------------------- 1 | .Spinner { 2 | background-image: url('./spinner.gif'); 3 | height: 16px; 4 | width: 16px; 5 | } -------------------------------------------------------------------------------- /src/components/Spinner.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | require('./Spinner.css'); 6 | 7 | var Spinner = React.createClass({ 8 | render: function() { 9 | return
; 10 | } 11 | }); 12 | 13 | module.exports = Spinner; -------------------------------------------------------------------------------- /src/components/Button.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var Button = React.createClass({ 6 | getDefaultProps: function() { 7 | return {href: 'javascript:;'}; 8 | }, 9 | 10 | render: function() { 11 | return this.transferPropsTo({this.props.children}); 12 | } 13 | }); 14 | 15 | module.exports = Button; -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | var Parse = require('parse').Parse; 2 | 3 | var AboutPage = require('./pages/AboutPage'); 4 | var ContentPage = require('./pages/ContentPage'); 5 | var HomePage = require('./pages/HomePage'); 6 | var ReactHack = require('ReactHack'); 7 | 8 | Parse.initialize('APPLICATION_ID', 'JAVASCRIPT_KEY'); 9 | 10 | ReactHack.start({ 11 | '': HomePage, 12 | 'pages/:name': ContentPage, 13 | 'pages/:name/:mode': ContentPage, 14 | 'about': AboutPage 15 | }); -------------------------------------------------------------------------------- /src/components/Markdown.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var showdown = require('showdown'); 6 | 7 | var converter = new showdown.converter(); 8 | 9 | var Markdown = React.createClass({ 10 | render: function() { 11 | return ( 12 |
17 | ); 18 | } 19 | }); 20 | 21 | module.exports = Markdown; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | loaders: [ 4 | { test: /\.css/, loader: "style-loader!css-loader" }, 5 | { test: /\.gif/, loader: "url-loader?limit=10000&minetype=image/gif" }, 6 | { test: /\.jpg/, loader: "url-loader?limit=10000&minetype=image/jpg" }, 7 | { test: /\.png/, loader: "url-loader?limit=10000&minetype=image/png" }, 8 | { test: /\.js$/, loader: "jsx-loader" } 9 | ], 10 | noParse: /parse-latest.js/ 11 | } 12 | }; -------------------------------------------------------------------------------- /src/pages/AboutPage.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var Layout = require('../layout/Layout.js'); 6 | 7 | var AboutPage = React.createClass({ 8 | render: function() { 9 | return ( 10 | 11 |

About ReactHack

12 |

This is a simple application built with React, Parse, and Bootstrap. Use it to get started building your application.

13 |
14 | ); 15 | } 16 | }); 17 | 18 | module.exports = AboutPage; -------------------------------------------------------------------------------- /src/layout/Layout.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var NavBar = require('./NavBar'); 6 | 7 | require('./Layout.css'); 8 | 9 | var Layout = React.createClass({ 10 | render: function() { 11 | return this.transferPropsTo( 12 |
13 | 14 |
15 |
16 | {this.props.children} 17 |
18 |
19 |
20 | ); 21 | } 22 | }); 23 | 24 | module.exports = Layout; -------------------------------------------------------------------------------- /src/pages/ContentPage.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var Content = require('../data/Content'); 6 | var ContentBlock = require('../components/ContentBlock'); 7 | var Layout = require('../layout/Layout'); 8 | 9 | var ContentPage = React.createClass({ 10 | render: function() { 11 | return ( 12 | 13 | 17 | 18 | ); 19 | } 20 | }); 21 | 22 | module.exports = ContentPage; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactHack", 3 | "version": "0.0.2", 4 | "description": "Get started with building React apps", 5 | "repository": "", 6 | "dependencies": { 7 | "parse": "petehunt/parsesdk-isomorphic", 8 | "react": "~0.8", 9 | "ReactHack": "petehunt/reacthack-core", 10 | "showdown": "petehunt/showdown" 11 | }, 12 | "devDependencies": { 13 | "jsx-loader": "~0.0.0", 14 | "webpack": "~0.11.0", 15 | "url-loader": "~0.5.1", 16 | "css-loader": "~0.6.2", 17 | "style-loader": "~0.6.0" 18 | }, 19 | "scripts": { 20 | "start": "webpack --watch --debug src/main.js build/main.js" 21 | }, 22 | "author": "Pete Hunt", 23 | "license": "Apache 2" 24 | } 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to ReactHack 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

If you see this, something is broken.

18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/layout/NavBar.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var NavBar = React.createClass({ 6 | render: function() { 7 | return this.transferPropsTo( 8 |
9 |
10 |
11 | 16 | ReactHack 17 |
18 | 22 |
23 |
24 |
25 |
26 | ); 27 | } 28 | }); 29 | 30 | module.exports = NavBar; -------------------------------------------------------------------------------- /src/components/NewPageModal.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var Modal = require('../components/Modal'); 6 | 7 | var NewPageModal = React.createClass({ 8 | getInitialState: function() { 9 | return {pageName: '', error: false}; 10 | }, 11 | 12 | componentWillReceiveProps: function(nextProps) { 13 | if (nextProps.visible && !this.props.visible) { 14 | // Reset the modal when it is opened. 15 | this.setState(this.getInitialState()); 16 | } 17 | }, 18 | 19 | handleChange: function(e) { 20 | // No whitespace allowed! 21 | this.setState({pageName: e.target.value.replace(' ', '_')}); 22 | }, 23 | 24 | handleAction: function() { 25 | if (!this.state.pageName) { 26 | this.setState({error: true}); 27 | } else { 28 | this.props.onNewPage(this.state.pageName); 29 | } 30 | 31 | // Prevent form submission 32 | return false; 33 | }, 34 | 35 | render: function() { 36 | return this.transferPropsTo( 37 | 38 |
39 | 45 |
46 |
47 | ); 48 | } 49 | }); 50 | 51 | module.exports = NewPageModal; -------------------------------------------------------------------------------- /src/data/Content.js: -------------------------------------------------------------------------------- 1 | var Parse = require('parse').Parse; 2 | 3 | var Content = Parse.Object.extend('Content', {}, { 4 | create: function(pageName) { 5 | var instance = new Content(); 6 | instance.set('pageName', pageName); 7 | instance.set('content', 'No content... *yet*.'); 8 | instance.save(); 9 | return instance; 10 | }, 11 | 12 | getByPageName: function(pageName, defaultContent, cb) { 13 | var collection = new Content.Collection(); 14 | collection.query = new Parse.Query(Content); 15 | collection.query.equalTo('pageName', pageName); 16 | collection.fetch({ 17 | success: function(obj) { 18 | cb(obj.models[0] || Content.create(pageName, defaultContent)); 19 | }, 20 | error: function(obj, err) { 21 | console.error('getByPageName() error', obj, err); 22 | } 23 | }); 24 | }, 25 | 26 | getAll: function(cb) { 27 | var collection = new Content.Collection(); 28 | collection.query = new Parse.Query(Content); 29 | collection.fetch({ 30 | success: function(obj) { 31 | cb(obj); 32 | }, 33 | error: function(obj, err) { 34 | console.error('getAll() error', obj, err); 35 | } 36 | }); 37 | } 38 | }); 39 | 40 | Content.Collection = Parse.Collection.extend({ 41 | model: Content, 42 | 43 | createContent: function(pageName) { 44 | this.add(Content.create(pageName)); 45 | } 46 | }); 47 | 48 | module.exports = Content; -------------------------------------------------------------------------------- /src/components/Modal.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var Modal = React.createClass({ 6 | render: function() { 7 | var actionButton = null; 8 | if (this.props.actionButton) { 9 | actionButton = ( 10 | 15 | ); 16 | } 17 | 18 | return this.transferPropsTo( 19 | 32 | ); 33 | }, 34 | 35 | componentDidMount: function() { 36 | $(this.getDOMNode()).modal({show: this.props.visible}); 37 | }, 38 | 39 | componentDidUpdate: function(prevProps) { 40 | if (this.props.visible !== prevProps.visible) { 41 | $(this.getDOMNode()).modal(this.props.visible ? 'show' : 'hide'); 42 | } 43 | } 44 | }); 45 | 46 | module.exports = Modal; -------------------------------------------------------------------------------- /src/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | var ReactHack = require('ReactHack'); 5 | 6 | var Button = require('../components/Button'); 7 | var Content = require('../data/Content'); 8 | var Layout = require('../layout/Layout'); 9 | var NewPageModal = require('../components/NewPageModal'); 10 | var Spinner = require('../components/Spinner'); 11 | 12 | var HomePage = React.createClass({ 13 | mixins: [ReactHack.FetchingMixin], 14 | 15 | modelState: ['pages'], 16 | fetchPollInterval: 60000, 17 | 18 | fetchData: function() { 19 | Content.getAll(this.stateSetter('pages')); 20 | }, 21 | 22 | getInitialState: function() { 23 | return {pages: null, modalShown: false}; 24 | }, 25 | 26 | handleClick: function() { 27 | this.setState({modalShown: true}); 28 | }, 29 | 30 | handleNewPage: function(name) { 31 | this.setState({modalShown: false}); 32 | this.state.pages.createContent(name); 33 | }, 34 | 35 | render: function() { 36 | var content; 37 | 38 | if (this.state.pages) { 39 | var links = this.state.pages.models.map(function(model) { 40 | var name = model.get('pageName'); 41 | return ( 42 |
  • {name}
  • 43 | ); 44 | }); 45 | content = ( 46 | 50 | ); 51 | } else { 52 | content = ; 53 | } 54 | 55 | return ( 56 | 57 | {content} 58 | 63 | 64 | ); 65 | } 66 | }); 67 | 68 | module.exports = HomePage; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Hackathon toolkit 2 | 3 | ![unmaintained](http://img.shields.io/badge/status-unmaintained-red.png) 4 | 5 | Build apps quickly using [React](http://facebook.github.io/react), [Bootstrap](http://getbootstrap.com/), [Parse / Backbone](http://parse.com/) and [webpack](http://webpack.github.io/) (formerly [Browserify](http://browserify.org/)). 6 | 7 | ## What is this? 8 | 9 | It's a simple app that lets you create Wiki-like pages using markdown and URL routing. It's easy to delete this functionality and start building your app. 10 | 11 | ## Getting started 12 | 13 | Make sure you have [npm](http://npmjs.org/). 14 | 15 | 1. `git clone https://github.com/petehunt/ReactHack.git` 16 | 2. `npm install` 17 | 3. Edit `src/main.js` to include your Parse API key. 18 | 4. `npm start` 19 | 5. Open `index.html` in your favorite browser 20 | 6. Start hacking! 21 | 22 | ## Find your way around 23 | 24 | * `src/main.js` - your routes and Parse API key 25 | * `src/layout` - general page layout components 26 | * `src/pages` - full-page components 27 | * `src/data` - Parse / Backbone Models and Collections 28 | * `src/components` - all other UI components 29 | 30 | ### Things you don't need to worry about 31 | * `src/framework` - the ReactHack code 32 | * `index.html` - the entry point to your app, just references the static resources you need 33 | * `build/main.js` - autogenerated by `npm start` 34 | 35 | ## FAQ 36 | 37 | ### It says "If you see this, something is broken." 38 | 39 | That means you didn't run `npm install` or `npm start`. 40 | 41 | ### There is an "Unauthorized" error in the browser error console 42 | 43 | That means you didn't edit `src/main.js` to include your Parse JavaScript API key. 44 | 45 | ### What!? I can `require()` CSS files!? 46 | 47 | Yes. Within your CSS files you can also `require()` images like so: 48 | 49 | ```css 50 | body { 51 | background-image: url('./myImage.jpg'); 52 | } 53 | ``` 54 | 55 | ## Future work 56 | 57 | - Full-page rendering 58 | - Server rendering 59 | - Testing integration 60 | - All Bootstrap-specific React components (specifically grids) 61 | -------------------------------------------------------------------------------- /src/components/ContentBlock.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | var ReactHack = require('ReactHack'); 5 | var Parse = require('parse').Parse; 6 | 7 | var Button = require('./Button'); 8 | var Content = require('../data/Content'); 9 | var Markdown = require('./Markdown'); 10 | var Spinner = require('../components/Spinner'); 11 | 12 | var ContentBlock = React.createClass({ 13 | mixins: [ReactHack.FetchingMixin], 14 | 15 | modelState: ['content'], 16 | 17 | getInitialState: function() { 18 | return {content: null, editableContent: null, loading: false}; 19 | }, 20 | 21 | shouldRefreshData: function(prevProps) { 22 | return this.props.name !== prevProps.name; 23 | }, 24 | 25 | fetchData: function() { 26 | Content.getByPageName( 27 | this.props.name, 28 | this.props.children, 29 | this.stateSetter('content') 30 | ); 31 | }, 32 | 33 | handleChange: function(e) { 34 | this.setState({editableContent: e.target.value}); 35 | }, 36 | 37 | getEditableContent: function() { 38 | // Example of a "computed property": either the user has changed the data 39 | // and it lives in this.state.editableContent, or they haven't, and it 40 | // lives in this.state.content. 41 | return this.state.editableContent || this.state.content.get('content'); 42 | }, 43 | 44 | handleSave: function() { 45 | this.state.content.set('content', this.getEditableContent()); 46 | this.setState({loading: true}); 47 | 48 | this.state.content.save(null, { 49 | success: function() { 50 | this.setState({loading: false}); 51 | Parse.history.navigate('#/pages/' + this.props.name, {trigger: true}); 52 | }.bind(this), 53 | 54 | error: function(obj, error) { 55 | console.error('Error saving', obj, error); 56 | } 57 | }); 58 | }, 59 | 60 | handleDelete: function() { 61 | this.setState({loading: true}); 62 | 63 | this.state.content.destroy({ 64 | success: function() { 65 | this.setState({loading: false}); 66 | Parse.history.navigate('#', {trigger: true}); 67 | }.bind(this), 68 | 69 | error: function(obj, error) { 70 | console.error('Error destroying', obj, error); 71 | } 72 | }); 73 | }, 74 | 75 | render: function() { 76 | if (!this.state.content) { 77 | return ; 78 | } 79 | 80 | if (!this.props.editing) { 81 | return ( 82 |
    83 | 84 | 87 | {this.state.content.get('content') || ''} 88 |
    89 | ); 90 | } 91 | 92 | var editableContent = this.getEditableContent(); 93 | 94 | if (this.state.saving) { 95 | return ( 96 |
    97 | 98 | {editableContent} 99 |
    100 | ); 101 | } 102 | 103 | return ( 104 |
    105 | 108 | 109 |