Test repo can now be used to list entries
This commit is contained in:
@ -32,7 +32,7 @@ export function loadConfig(config) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(configLoading());
|
||||
|
||||
fetch('config.yml').then((response) => {
|
||||
fetch('/config.yml').then((response) => {
|
||||
if (response.status !== 200) {
|
||||
throw `Failed to load config.yml (${response.status})`;
|
||||
}
|
||||
|
49
src/actions/entries.js
Normal file
49
src/actions/entries.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { currentBackend } from '../backends/backend';
|
||||
|
||||
export const ENTRIES_REQUEST = 'ENTRIES_REQUEST';
|
||||
export const ENTRIES_SUCCESS = 'ENTRIES_SUCCESS';
|
||||
export const ENTRIES_FAILURE = 'ENTRIES_FAILURE';
|
||||
|
||||
export function entriesLoaded(collection, entries) {
|
||||
return {
|
||||
type: ENTRIES_SUCCESS,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
entries: entries
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function entriesLoading(collection) {
|
||||
return {
|
||||
type: ENTRIES_REQUEST,
|
||||
payload: {
|
||||
collection: collection.get('name')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function entriesFailed(collection, error) {
|
||||
return {
|
||||
type: ENTRIES_FAILURE,
|
||||
error: 'Failed to load entries',
|
||||
payload: error.toString(),
|
||||
meta: {collection: collection.get('name')}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadEntries(collection) {
|
||||
return (dispatch, getState) => {
|
||||
if (collection.get('isFetching')) { return; }
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
|
||||
dispatch(entriesLoading(collection));
|
||||
backend.entries(collection)
|
||||
.then((entries) => dispatch(entriesLoaded(collection, entries)))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return dispatch(entriesFailed(collection, err));
|
||||
});
|
||||
};
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import TestRepoBackend from './test-repo/Implementation';
|
||||
import { resolveFormat } from '../formats/formats';
|
||||
|
||||
export function resolveBackend(config) {
|
||||
const name = config.getIn(['backend', 'name']);
|
||||
@ -32,8 +33,24 @@ class Backend {
|
||||
return this.implementation.authComponent();
|
||||
}
|
||||
|
||||
authenticate(state) {
|
||||
return this.implementation.authenticate(state);
|
||||
authenticate(credentials) {
|
||||
return this.implementation.authenticate(credentials);
|
||||
}
|
||||
|
||||
entries(collection) {
|
||||
return this.implementation.entries(collection).then((entries) => (
|
||||
(entries || []).map((entry) => {
|
||||
const format = resolveFormat(collection, entry);
|
||||
if (entry && entry.raw) {
|
||||
entry.data = format && format.fromFile(entry.raw);
|
||||
}
|
||||
return entry;
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
entry(collection, slug) {
|
||||
return this.implementation.entry(collection, slug);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,8 @@ export default class AuthenticationPage extends React.Component {
|
||||
this.handleEmailChange = this.handleEmailChange.bind(this);
|
||||
}
|
||||
|
||||
handleLogin() {
|
||||
handleLogin(e) {
|
||||
e.preventDefault();
|
||||
this.props.onLogin(this.state);
|
||||
}
|
||||
|
||||
@ -21,13 +22,13 @@ export default class AuthenticationPage extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
return <form onSubmit={this.handleLogin}>
|
||||
<p>
|
||||
<label>Your name or email: <input type='text' onChange={this.handleEmailChange}/></label>
|
||||
</p>
|
||||
<p>
|
||||
<button onClick={this.handleLogin}>Login</button>
|
||||
<button type='submit'>Login</button>
|
||||
</p>
|
||||
</div>;
|
||||
</form>;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,16 @@
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
function getSlug(path) {
|
||||
const m = path.match(/([^\/]+)(\.[^\/\.]+)?$/);
|
||||
return m && m[1];
|
||||
}
|
||||
|
||||
export default class TestRepo {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
if (window.repoFiles == null) {
|
||||
throw 'The TestRepo backend needs a "window.repoFiles" object.';
|
||||
}
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
@ -12,4 +20,24 @@ export default class TestRepo {
|
||||
authenticate(state) {
|
||||
return Promise.resolve({email: state.email});
|
||||
}
|
||||
|
||||
entries(collection) {
|
||||
const entries = [];
|
||||
const folder = collection.get('folder');
|
||||
if (folder) {
|
||||
for (var path in window.repoFiles[folder]) {
|
||||
entries.push({
|
||||
path: folder + '/' + path,
|
||||
slug: getSlug(path),
|
||||
raw: window.repoFiles[folder][path].content
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(entries);
|
||||
}
|
||||
|
||||
entry(collection, slug) {
|
||||
return Promise.resolve({slug: slug, title: 'hello'});
|
||||
}
|
||||
}
|
||||
|
19
src/components/EntryListing.js
Normal file
19
src/components/EntryListing.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
export default class EntryListing extends React.Component {
|
||||
render() {
|
||||
const { collection, entries } = this.props;
|
||||
const name = collection.get('name');
|
||||
|
||||
return <div>
|
||||
<h2>Listing entries!</h2>
|
||||
{entries.map((entry) => {
|
||||
const path = `/collections/${name}/entries/${entry.get('slug')}`;
|
||||
return <Link key={entry.get('slug')} to={path}>
|
||||
<h3>{entry.getIn(['data', 'title'])}</h3>
|
||||
</Link>;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
}
|
61
src/containers/CollectionPage.js
Normal file
61
src/containers/CollectionPage.js
Normal file
@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import { loadEntries } from '../actions/entries';
|
||||
import EntryListing from '../components/EntryListing';
|
||||
|
||||
class DashboardPage extends React.Component {
|
||||
componentDidMount() {
|
||||
const { collection, dispatch } = this.props;
|
||||
|
||||
if (collection) {
|
||||
dispatch(loadEntries(collection));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { collection, dispatch } = this.props;
|
||||
if (nextProps.collection !== collection) {
|
||||
dispatch(loadEntries(nextProps.collection));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { collections, collection, slug, children } = this.props;
|
||||
|
||||
if (collections == null) {
|
||||
return <h1>No collections defined in your config.yml</h1>;
|
||||
}
|
||||
|
||||
const entries = collection.get('entries');
|
||||
|
||||
return <div>
|
||||
<h1>Dashboard</h1>
|
||||
<div>
|
||||
{collections.map((collection) => (
|
||||
<div key={collection.get('name')}>
|
||||
<Link to={`/collections/${collection.get('name')}`}>{collection.get('name')}</Link>
|
||||
</div>
|
||||
)).toArray()}
|
||||
</div>
|
||||
<div>
|
||||
{slug ? children :
|
||||
entries ? <EntryListing collection={collection} entries={entries}/> : 'No entries...'
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const { collections } = state;
|
||||
const { name, slug } = ownProps.params;
|
||||
|
||||
return {
|
||||
slug: slug,
|
||||
collection: name ? collections.get(name) : collections.first(),
|
||||
collections: collections
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(DashboardPage);
|
@ -1,26 +0,0 @@
|
||||
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);
|
33
src/containers/EntryPage.js
Normal file
33
src/containers/EntryPage.js
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import { loadEntries } from '../actions/entries';
|
||||
import EntryListing from '../components/EntryListing';
|
||||
|
||||
class DashboardPage extends React.Component {
|
||||
componentDidMount() {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { collection, entry } = this.props;
|
||||
|
||||
return <div>
|
||||
<h1>Entry in {collection.get('label')}</h1>
|
||||
<h2>{entry && entry.get('title')}</h2>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const { collections } = state;
|
||||
|
||||
return {
|
||||
collection: collections.get(ownProps.params.name),
|
||||
collections: collections
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(DashboardPage);
|
5
src/formats/formats.js
Normal file
5
src/formats/formats.js
Normal file
@ -0,0 +1,5 @@
|
||||
import YAMLFrontmatter from './yaml-frontmatter';
|
||||
|
||||
export function resolveFormat(collection, entry) {
|
||||
return new YAMLFrontmatter();
|
||||
}
|
31
src/formats/yaml-frontmatter.js
Normal file
31
src/formats/yaml-frontmatter.js
Normal file
@ -0,0 +1,31 @@
|
||||
import YAML from './yaml';
|
||||
|
||||
const regexp = /^---\n([^]*?)\n---\n([^]*)$/;
|
||||
|
||||
export default class YAMLFrontmatter {
|
||||
fromFile(content) {
|
||||
const match = content.match(regexp);
|
||||
const obj = match ? new YAML().fromFile(match[1]) : {};
|
||||
obj.body = match ? (match[2] || '').replace(/^\n+/, '') : content;
|
||||
return obj;
|
||||
}
|
||||
|
||||
toFile(data) {
|
||||
const meta = {};
|
||||
let body = '';
|
||||
let content = '';
|
||||
for (var key in data) {
|
||||
if (key === 'body') {
|
||||
body = data[key];
|
||||
} else {
|
||||
meta[key] = data[key];
|
||||
}
|
||||
}
|
||||
|
||||
content += '---\n';
|
||||
content += new YAML().toFile(meta);
|
||||
content += '---\n\n';
|
||||
content += body;
|
||||
return content;
|
||||
}
|
||||
}
|
31
src/formats/yaml.js
Normal file
31
src/formats/yaml.js
Normal file
@ -0,0 +1,31 @@
|
||||
import yaml from 'js-yaml';
|
||||
import moment from 'moment';
|
||||
|
||||
const MomentType = new yaml.Type('date', {
|
||||
kind: 'scalar',
|
||||
predicate: function(value) {
|
||||
return moment.isMoment(value);
|
||||
},
|
||||
represent: function(value) {
|
||||
return value.format(value._f);
|
||||
},
|
||||
resolve: function(value) {
|
||||
return moment.isMoment(value) && value._f;
|
||||
}
|
||||
});
|
||||
|
||||
const OutputSchema = new yaml.Schema({
|
||||
include: yaml.DEFAULT_SAFE_SCHEMA.include,
|
||||
implicit: [MomentType].concat(yaml.DEFAULT_SAFE_SCHEMA.implicit),
|
||||
explicit: yaml.DEFAULT_SAFE_SCHEMA.explicit
|
||||
});
|
||||
|
||||
export default class YAML {
|
||||
fromFile(content) {
|
||||
return yaml.safeLoad(content);
|
||||
}
|
||||
|
||||
toFile(data) {
|
||||
return yaml.safeDump(data, {schema: OutputSchema});
|
||||
}
|
||||
}
|
@ -1,15 +1,20 @@
|
||||
import Immutable from 'immutable';
|
||||
import { OrderedMap, fromJS } from 'immutable';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
import { ENTRIES_REQUEST, ENTRIES_SUCCESS } from '../actions/entries';
|
||||
|
||||
export function collections(state = null, action) {
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS:
|
||||
const collections = action.payload && action.payload.collections;
|
||||
return Immutable.OrderedMap().withMutations((map) => {
|
||||
return OrderedMap().withMutations((map) => {
|
||||
(collections || []).forEach(function(collection) {
|
||||
map.set(collection.name, Immutable.fromJS(collection));
|
||||
map.set(collection.name, fromJS(collection));
|
||||
});
|
||||
});
|
||||
case ENTRIES_REQUEST:
|
||||
return state && state.setIn([action.payload.collection, 'isFetching'], true);
|
||||
case ENTRIES_SUCCESS:
|
||||
return state && state.setIn([action.payload.collection, 'entries'], fromJS(action.payload.entries));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -1,13 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
|
||||
import App from '../containers/App';
|
||||
import DashboardPage from '../containers/DashboardPage';
|
||||
import DashboardPage from '../containers/CollectionPage';
|
||||
import EntryPage from '../containers/EntryPage';
|
||||
import NotFoundPage from '../containers/NotFoundPage';
|
||||
|
||||
export default () => (
|
||||
<Router history={browserHistory}>
|
||||
<Route path="/" component={App}>
|
||||
<IndexRoute component={DashboardPage}/>
|
||||
<Route path="/collections/:name" component={DashboardPage}>
|
||||
<Route path="/collections/:name/entries/:slug" component={EntryPage}/>
|
||||
</Route>
|
||||
<Route path="*" component={NotFoundPage}/>
|
||||
</Route>
|
||||
</Router>
|
||||
|
Reference in New Issue
Block a user