Initial commit

This commit is contained in:
Mathias Biilmann Christensen 2016-02-25 00:45:56 -08:00
commit c60d8ba706
19 changed files with 590 additions and 0 deletions

4
.babelrc Normal file
View File

@ -0,0 +1,4 @@
{
"presets": ["react", "es2015"],
"plugins": ["transform-class-properties", "transform-object-assign", "transform-object-rest-spread"]
}

125
.eslintrc Normal file
View File

@ -0,0 +1,125 @@
env:
browser: true
parser: babel-eslint
plugins: [ "react" ]
# enable ECMAScript features
ecmaFeatures:
arrowFunctions: true
binaryLiterals: true
blockBindings: true
classes: true
defaultParams: true
destructuring: true
forOf: true
generators: true
jsx: true
modules: true
objectLiteralShorthandMethods: true
objectLiteralShorthandProperties: true
octalLiterals: true
spread: true
templateStrings: true
rules:
# Possible Errors
# https://github.com/eslint/eslint/tree/master/docs/rules#possible-errors
no-control-regex: 2
no-debugger: 2
no-dupe-args: 2
no-dupe-keys: 2
no-duplicate-case: 2
no-empty-character-class: 2
no-ex-assign: 2
no-extra-boolean-cast : 2
no-extra-semi: 2
no-invalid-regexp: 2
no-irregular-whitespace: 2
no-proto: 2
no-unexpected-multiline: 2
no-unreachable: 2
valid-typeof: 2
# Best Practices
# https://github.com/eslint/eslint/tree/master/docs/rules#best-practices
no-fallthrough: 2
no-redeclare: 2
# Stylistic Issues
# https://github.com/eslint/eslint/tree/master/docs/rules#stylistic-issues
comma-spacing: 2
eol-last: 2
indent: [2, 2, {SwitchCase: 1}]
max-len: [2, 160, 2]
new-parens: 2
no-mixed-spaces-and-tabs: 2
no-multiple-empty-lines: [2, {max: 2}]
no-trailing-spaces: 2
quotes: [2, "single", "avoid-escape"]
semi: 2
space-after-keywords: 2
space-before-blocks: [2, "always"]
space-before-function-paren: [2, "never"]
space-in-parens: [2, "never"]
space-infix-ops: 2
space-return-throw-case: 2
space-unary-ops: 2
# ECMAScript 6
# http://eslint.org/docs/rules/#ecmascript-6
arrow-parens: [2, "always"]
arrow-spacing: [2, {"before": true, "after": true}]
no-arrow-condition: 2
prefer-const: 2
# Strict Mode
# https://github.com/eslint/eslint/tree/master/docs/rules#strict-mode
strict: [2, "global"]
# Variables
# https://github.com/eslint/eslint/tree/master/docs/rules#variables
no-undef: 2
no-unused-vars: [2, {"args": "none"}]
react/forbid-prop-types: 1
react/jsx-boolean-value: 1
react/jsx-closing-bracket-location: 1
react/jsx-curly-spacing: 1
react/jsx-equals-spacing: 1
react/jsx-handler-names: 1
react/jsx-indent-props: 1
react/jsx-indent: [2, 2]
react/jsx-no-bind: 1
react/jsx-no-duplicate-props: 1
react/jsx-no-undef: 1
react/jsx-pascal-case: 1
react/jsx-sort-prop-types: 1
react/jsx-uses-react: 1
react/jsx-uses-vars: 1
react/no-danger: 1
react/no-deprecated: 1
react/no-did-mount-set-state: 1
react/no-did-update-set-state: 1
react/no-direct-mutation-state: 1
react/no-is-mounted: 1
react/no-multi-comp: 1
react/no-string-refs: 1
react/no-unknown-property: 1
react/prefer-es6-class: 1
react/react-in-jsx-scope: 1
react/require-extension: 1
react/self-closing-comp: 1
react/sort-comp: 1
# Global scoped method and vars
globals:
netlify: true
describe: true
it: true
require: true
process: true
module: true
CMS_ENV: true

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
dist/
node_modules/
npm-debug.log

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
4

56
example/config.yml Normal file
View File

@ -0,0 +1,56 @@
backend:
name: test-repo
delay: 0.1
media_folder: "assets/uploads"
collections: # A list of collections the CMS should be able to edit
- name: "posts" # Used in routes, ie.: /admin/collections/:slug/edit
label: "Post" # Used in the UI, ie.: "New Post"
folder: "_posts"
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
create: true # Allow users to create new documents in this collection
fields: # The fields each document in this collection have
- {label: "Title", name: "title", widget: "string", tagname: "h1"}
- {label: "Cover Image", name: "image", widget: "image", optional: true, tagname: ""}
- {label: "Body", name: "body", widget: "markdown"}
meta:
- {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"}
- {label: "SEO Description", name: "description", widget: "text"}
- name: "faq" # Used in routes, ie.: /admin/collections/:slug/edit
label: "FAQ" # Used in the UI, ie.: "New Post"
folder: "_faqs"
create: true # Allow users to create new documents in this collection
fields: # The fields each document in this collection have
- {label: "Question", name: "title", widget: "string", tagname: "h1"}
- {label: "Answer", name: "body", widget: "markdown"}
- name: "settings"
label: "Settings"
files:
- name: "general"
label: "Site Settings"
file: "_data/settings.json"
description: "General Site Settings"
fields:
- {label: "Global title", name: site_title, widget: "string"}
- label: "Post Settings"
name: posts
widget: "object"
fields:
- {label: "Number of posts on frontpage", name: front_limit, widget: number}
- {label: "Default Author", name: author, widget: string}
- {label: "Default Thumbnail", name: thumb, widget: image, class: "thumb"}
- name: "authors"
label: "Authors"
file: "_data/authors.yml"
description: "Author descriptions"
fields:
- name: authors
label: Authors
widget: list
fields:
- {label: "Name", name: "name", widget: "string"}
- {label: "Description", name: "description", widget: "markdown"}

10
example/index.html Normal file
View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>This is an example</title>
</head>
<body>
<script src='/cms.js'></script>
</body>
</html>

57
package.json Normal file
View File

@ -0,0 +1,57 @@
{
"name": "netlify-react-cms",
"version": "0.0.1",
"description": "Netlify CMS lets content editors work on structured content stored in git",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server -d --inline --config webpack.config.js",
"test": "NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./test/setup.js",
"test:watch": "npm test -- --watch"
},
"keywords": [
"netlify",
"cms"
],
"author": "Netlify",
"license": "MIT",
"devDependencies": {
"autoprefixer": "^6.3.3",
"babel-core": "^6.5.1",
"babel-eslint": "^4.1.8",
"babel-loader": "^6.2.2",
"babel-plugin-transform-class-properties": "^6.5.2",
"babel-plugin-transform-object-assign": "^6.5.0",
"babel-plugin-transform-object-rest-spread": "^6.5.0",
"babel-preset-es2015": "^6.5.0",
"babel-preset-react": "^6.5.0",
"babel-register": "^6.5.2",
"babel-runtime": "^6.5.0",
"eslint": "^1.10.3",
"eslint-loader": "^1.2.1",
"eslint-plugin-react": "^3.16.1",
"exports-loader": "^0.6.3",
"express": "^4.13.4",
"file-loader": "^0.8.5",
"immutable": "^3.7.6",
"imports-loader": "^0.6.5",
"js-yaml": "^3.5.3",
"mocha": "^2.4.5",
"normalizr": "^2.0.0",
"react": "^0.14.7",
"react-dom": "^0.14.7",
"react-immutable-proptypes": "^1.6.0",
"react-lazy-load": "^3.0.3",
"react-pure-render": "^1.0.2",
"react-redux": "^4.4.0",
"react-router": "^2.0.0",
"react-router-redux": "^3.0.0",
"redux": "^3.3.1",
"redux-thunk": "^1.0.3",
"style-loader": "^0.13.0",
"url-loader": "^0.5.7",
"webpack": "^1.12.13",
"webpack-dev-server": "^1.14.1",
"webpack-postcss-tools": "^1.1.1",
"whatwg-fetch": "^0.11.0"
}
}

60
src/actions/config.js Normal file
View File

@ -0,0 +1,60 @@
import yaml from 'js-yaml';
export const CONFIG = {
REQUEST: 'REQUEST',
SUCCESS: 'SUCCESS',
FAILURE: 'FAILURE'
};
export function configLoaded(config) {
return {
type: CONFIG.SUCCESS,
payload: config
};
}
export function configLoading() {
return {
type: CONFIG.REQUEST
};
}
export function configFailed(err) {
return {
type: CONFIG.FAILURE,
error: 'Error loading config',
payload: err
};
}
export function loadConfig(config) {
if (window.CMS_CONFIG) {
return configLoaded(window.CMS_CONFIG);
}
return (dispatch, getState) => {
dispatch(configLoading());
fetch('config.yml').then((response) => {
if (response.status !== 200) {
throw `Failed to load config.yml (${response.status})`;
}
response.text().then(parseConfig).then((config) => dispatch(configLoaded(config)));
}).catch((err) => {
dispatch(configFailed(err));
});
};
}
function parseConfig(data) {
const config = yaml.safeLoad(data);
if (typeof CMS_ENV === 'string' && config[CMS_ENV]) {
for (var key in config[CMS_ENV]) {
if (config[CMS_ENV].hasOwnProperty(key)) {
config[key] = config[CMS_ENV][key];
}
}
}
return config;
}

58
src/containers/App.js Normal file
View File

@ -0,0 +1,58 @@
import React from 'react';
import { connect } from 'react-redux';
import { loadConfig } from '../actions/config';
class App extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.dispatch(loadConfig());
}
configError(config) {
return <div>
<h1>Error loading the CMS configuration</h1>
<div>
<p>The "config.yml" file could not be loaded or failed to parse properly.</p>
<p><strong>Error message:</strong> {config.get('error')}</p>
</div>
</div>;
}
configLoading() {
return <div>
<h1>Loading configuration...</h1>
</div>;
}
render() {
const { config, children } = this.props;
if (config === null) {
return null;
}
if (config.get('error')) {
return this.configError(config);
}
if (config.get('isFetching')) {
return this.configLoading();
}
return (
<div>{children}</div>
);
}
}
function mapStateToProps(state) {
return {
config: state.config
};
}
export default connect(mapStateToProps)(App);

View File

@ -0,0 +1,26 @@
import React from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
class DashboardPage extends React.Component {
render() {
const { collections } = this.props;
return <div>
<h1>Dashboard</h1>
{collections && collections.map((collection) => (
<div key={collection.get('name')}>
<Link to={`/collections/${collection.get('name')}`}>{collection.get('name')}</Link>
</div>
)).toArray()}
</div>;
}
}
function mapStateToProps(state) {
return {
collections: state.collections
};
}
export default connect(mapStateToProps)(DashboardPage);

17
src/index.js Normal file
View File

@ -0,0 +1,17 @@
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
import Routes from './routes/routes';
import 'file?name=index.html!../example/index.html';
const store = configureStore();
const el = document.createElement('div');
document.body.appendChild(el);
render((
<Provider store={store}>
<Routes/>
</Provider>
), el);

View File

@ -0,0 +1,16 @@
import Immutable from 'immutable';
import { CONFIG } from '../actions/config';
export function collections(state = null, action) {
switch (action.type) {
case CONFIG.SUCCESS:
const collections = action.payload && action.payload.collections;
return Immutable.OrderedMap().withMutations((map) => {
(collections || []).forEach(function(collection) {
map.set(collection.name, Immutable.fromJS(collection));
});
});
default:
return state;
}
}

15
src/reducers/config.js Normal file
View File

@ -0,0 +1,15 @@
import Immutable from 'immutable';
import { CONFIG } from '../actions/config';
export function config(state = null, action) {
switch (action.type) {
case CONFIG.REQUEST:
return Immutable.Map({isFetching: true});
case CONFIG.SUCCESS:
return Immutable.fromJS(action.payload);
case CONFIG.FAILURE:
return Immutable.Map({error: action.payload.toString()});
default:
return state;
}
}

12
src/routes/routes.js Normal file
View File

@ -0,0 +1,12 @@
import React from 'react';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import App from '../containers/App';
import DashboardPage from '../containers/DashboardPage';
export default () => (
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={DashboardPage}/>
</Route>
</Router>
);

View File

@ -0,0 +1,21 @@
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { browserHistory } from 'react-router';
import { syncHistory, routeReducer } from 'react-router-redux';
import { config } from '../reducers/config';
import { collections } from '../reducers/collections';
const reducer = combineReducers({
config,
collections,
router: routeReducer
});
const createStoreWithMiddleware = compose(
applyMiddleware(thunkMiddleware, syncHistory(browserHistory)),
window.devToolsExtension ? window.devToolsExtension() : (f) => f
)(createStore);
export default (initialState) => (
createStoreWithMiddleware(reducer, initialState)
);

View File

@ -0,0 +1,26 @@
import expect from 'expect';
import Immutable from 'immutable';
import { configLoaded } from '../../src/actions/config';
import { collections } from '../../src/reducers/collections';
describe('collections', () => {
it('should handle an empty state', () => {
expect(
collections(undefined, {})
).toEqual(
null
);
});
it('should load the collections from the config', () => {
expect(
collections(undefined, configLoaded({collections: [
{name: 'posts', folder: '_posts', fields: [{name: 'title', widget: 'string'}]}
]}))
).toEqual(
Immutable.OrderedMap({
posts: Immutable.fromJS({name: 'posts', folder: '_posts', fields: [{name: 'title', widget: 'string'}]})
})
);
});
});

View File

@ -0,0 +1,38 @@
import expect from 'expect';
import Immutable from 'immutable';
import { configLoaded, configLoading, configFailed } from '../../src/actions/config';
import { config } from '../../src/reducers/config';
describe('config', () => {
it('should handle an empty state', () => {
expect(
config(undefined, {})
).toEqual(
null
);
});
it('should handle an update', () => {
expect(
config(Immutable.Map({'a': 'b', 'c': 'd'}), configLoaded({'a': 'changed', 'e': 'new'}))
).toEqual(
Immutable.Map({'a': 'changed', 'e': 'new'})
);
});
it('should mark the config as loading', () => {
expect(
config(undefined, configLoading())
).toEqual(
Immutable.Map({isFetching: true})
);
});
it('should handle an error', () => {
expect(
config(Immutable.Map({isFetching: true}), configFailed(new Error('Config could not be loaded')))
).toEqual(
Immutable.Map({error: 'Error: Config could not be loaded'})
);
});
});

0
test/setup.js Normal file
View File

45
webpack.config.js Normal file
View File

@ -0,0 +1,45 @@
/* global module, __dirname, require */
var webpack = require('webpack');
var path = require('path');
module.exports = {
module: {
loaders: [
{
test: /\.((png)|(eot)|(woff)|(woff2)|(ttf)|(svg)|(gif))(\?v=\d+\.\d+\.\d+)?$/,
loader: 'file?name=/[hash].[ext]'
},
{
loader: 'babel',
test: /\.js?$/,
exclude: /(node_modules|bower_components)/,
query: {
cacheDirectory: true,
presets: ['react', 'es2015'],
plugins: ['transform-class-properties', 'transform-object-assign', 'transform-object-rest-spread']
}
}
]
},
plugins: [
new webpack.ProvidePlugin({
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
})
],
context: path.join(__dirname, 'src'),
entry: {
cms: './index',
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
externals: [/^vendor\/.+\.js$/],
devServer: {
contentBase: 'example/',
historyApiFallback: true,
devTool: 'source-map'
},
};