Initial commit
This commit is contained in:
commit
c60d8ba706
4
.babelrc
Normal file
4
.babelrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["react", "es2015"],
|
||||
"plugins": ["transform-class-properties", "transform-object-assign", "transform-object-rest-spread"]
|
||||
}
|
125
.eslintrc
Normal file
125
.eslintrc
Normal 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
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
npm-debug.log
|
56
example/config.yml
Normal file
56
example/config.yml
Normal 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
10
example/index.html
Normal 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
57
package.json
Normal 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
60
src/actions/config.js
Normal 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
58
src/containers/App.js
Normal 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);
|
26
src/containers/DashboardPage.js
Normal file
26
src/containers/DashboardPage.js
Normal 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
17
src/index.js
Normal 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);
|
16
src/reducers/collections.js
Normal file
16
src/reducers/collections.js
Normal 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
15
src/reducers/config.js
Normal 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
12
src/routes/routes.js
Normal 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>
|
||||
);
|
21
src/store/configureStore.js
Normal file
21
src/store/configureStore.js
Normal 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)
|
||||
);
|
26
test/reducers/collections.spec.js
Normal file
26
test/reducers/collections.spec.js
Normal 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'}]})
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
38
test/reducers/config.spec.js
Normal file
38
test/reducers/config.spec.js
Normal 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
0
test/setup.js
Normal file
45
webpack.config.js
Normal file
45
webpack.config.js
Normal 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'
|
||||
},
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user