├── 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 |
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 |
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 |
20 |
21 |
22 |
{this.props.title}
23 |
24 |
25 | {this.props.children}
26 |
27 |
28 |
29 | {actionButton}
30 |
31 |
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 |
47 | {links}
48 |
49 |
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 | 
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 |
110 | {editableContent}
111 |
112 | );
113 | }
114 | });
115 |
116 | module.exports = ContentBlock;
--------------------------------------------------------------------------------