Merge branch 'contentIntegration' into react
This commit is contained in:
@ -17,6 +17,7 @@ collections: # A list of collections the CMS should be able to edit
|
|||||||
meta:
|
meta:
|
||||||
- {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"}
|
- {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"}
|
||||||
- {label: "SEO Description", name: "description", widget: "text"}
|
- {label: "SEO Description", name: "description", widget: "text"}
|
||||||
|
card: {type: "image", image: "image", text: "title"}
|
||||||
|
|
||||||
- name: "faq" # Used in routes, ie.: /admin/collections/:slug/edit
|
- name: "faq" # Used in routes, ie.: /admin/collections/:slug/edit
|
||||||
label: "FAQ" # Used in the UI, ie.: "New Post"
|
label: "FAQ" # Used in the UI, ie.: "New Post"
|
||||||
@ -25,6 +26,7 @@ collections: # A list of collections the CMS should be able to edit
|
|||||||
fields: # The fields each document in this collection have
|
fields: # The fields each document in this collection have
|
||||||
- {label: "Question", name: "title", widget: "string", tagname: "h1"}
|
- {label: "Question", name: "title", widget: "string", tagname: "h1"}
|
||||||
- {label: "Answer", name: "body", widget: "markdown"}
|
- {label: "Answer", name: "body", widget: "markdown"}
|
||||||
|
card: {type: "alltype", text: "title"}
|
||||||
|
|
||||||
- name: "settings"
|
- name: "settings"
|
||||||
label: "Settings"
|
label: "Settings"
|
||||||
|
@ -4,8 +4,9 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
<title>This is an example</title>
|
<title>This is an example</title>
|
||||||
<link rel="stylesheet" href="https://facebook.github.io/draft-js/css/draft.css"/>
|
|
||||||
<link href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700' rel='stylesheet' type='text/css'>
|
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500' rel='stylesheet' type='text/css'>
|
||||||
|
<link rel="stylesheet" href="/cms.css"/>
|
||||||
<script>
|
<script>
|
||||||
window.repoFiles = {
|
window.repoFiles = {
|
||||||
_posts: {
|
_posts: {
|
||||||
@ -16,6 +17,15 @@
|
|||||||
_faqs: {
|
_faqs: {
|
||||||
"what-is-netlify-cms.md": {
|
"what-is-netlify-cms.md": {
|
||||||
content: "---\ntitle: What is netlify CMS?\ndate: 2015-11-02T00:00.000Z\n---\n\n# Netlify CMS is Content Manager for Static Site Generators\n\nStatic sites are many times faster, cheaper and safer and traditional dynamic websites.\n\nModern static site generators like Jekyll, Middleman, Roots or Hugo are powerful publishing and development systems, but when we build sites for non-technical users, we need a layer on top of them.\n\nNetlify CMS is there to let your marketing team push new content to your public site, or to let technical writers work on your documentation.\n\nNetlify CMS integrates with Git and turns normal content editors into git comitters.\n\n"
|
content: "---\ntitle: What is netlify CMS?\ndate: 2015-11-02T00:00.000Z\n---\n\n# Netlify CMS is Content Manager for Static Site Generators\n\nStatic sites are many times faster, cheaper and safer and traditional dynamic websites.\n\nModern static site generators like Jekyll, Middleman, Roots or Hugo are powerful publishing and development systems, but when we build sites for non-technical users, we need a layer on top of them.\n\nNetlify CMS is there to let your marketing team push new content to your public site, or to let technical writers work on your documentation.\n\nNetlify CMS integrates with Git and turns normal content editors into git comitters.\n\n"
|
||||||
|
},
|
||||||
|
"what-is-jam-stack.md": {
|
||||||
|
content: "---\ntitle: What is the “JAM Stack”?\ndate: 2015-11-02T00:00.000Z\n---\n\n# The JAM stack is a new way of building websites and apps that are fast, secure and simple to work with.\n\nJAM stands for JavaScript, APIs and Markup. It's the fastest growing new stack for building websites and apps: no more servers, host all your front-end on a CDN and use APIs for any moving parts.\n\n"
|
||||||
|
},
|
||||||
|
"cache-invalidation.md": {
|
||||||
|
content: "---\ntitle: What about Cache Invalidation?\ndate: 2015-11-02T00:00.000Z\n---\n\n# Netlify handles cache invalidation automatically\n\nWhen your changes go live, they go live.\n\nNo waiting for cache purges, no cumbersome varnish setup, no API calls to clean your distribution. Netlify handles cache purges within an average of 250ms from your deploy!\n\n"
|
||||||
|
},
|
||||||
|
"continuous-deployment.md": {
|
||||||
|
content: "---\ntitle: Does Netlify support Continuous Deployment?\ndate: 2015-11-02T00:00.000Z\n---\n\n# Yes, Netlify let you Integrate your site or web-app to GitHub, GitLab or BitBucket and run your build tool on our servers.\n\nAutomatically rebuild your site every time your content changes: trigger builds by pushing to git or via webhooks.\n\n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_data: {
|
_data: {
|
||||||
@ -30,7 +40,7 @@
|
|||||||
|
|
||||||
var ONE_DAY = 60 * 60 * 24 * 1000;
|
var ONE_DAY = 60 * 60 * 24 * 1000;
|
||||||
|
|
||||||
for (var i= 0; i<10; i++) {
|
for (var i=1; i<=10; i++) {
|
||||||
var date = new Date();
|
var date = new Date();
|
||||||
|
|
||||||
date.setTime(date.getTime() + ONE_DAY);
|
date.setTime(date.getTime() + ONE_DAY);
|
||||||
@ -38,12 +48,25 @@
|
|||||||
var slug = dateString + "-post-number-" + i + ".md";
|
var slug = dateString + "-post-number-" + i + ".md";
|
||||||
|
|
||||||
window.repoFiles._posts[slug] = {
|
window.repoFiles._posts[slug] = {
|
||||||
content: "---\ntitle: \"This is post # " + (10-i) + "\"\ndate: " + dateString + "T00:99:99.999Z\n---\n\n# The post is number " + i + "\n\nAnd this is yet another identical post body"
|
content: "---\ntitle: \"This is post # " + i + "\"\ndate: " + dateString + "T00:99:99.999Z\n---\n\n# The post is number " + i + "\n\nAnd this is yet another identical post body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i=1; i<=5; i++) {
|
||||||
|
var date = new Date();
|
||||||
|
|
||||||
|
date.setTime(date.getTime() + ONE_DAY);
|
||||||
|
var dateString = '' + date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
|
||||||
|
var slug = dateString + "-faq-number-" + i + ".md";
|
||||||
|
|
||||||
|
window.repoFiles._faqs[slug] = {
|
||||||
|
content: "---\ntitle: \"This FAQ item # " + i + "\"\ndate: " + dateString + "T00:99:99.999Z\n---\n\n# Loren ipsum dolor sit amet"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<script src='/cms.js'></script>
|
<script src='/cms.js'></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
10
package.json
10
package.json
@ -36,6 +36,7 @@
|
|||||||
"eslint-loader": "^1.2.1",
|
"eslint-loader": "^1.2.1",
|
||||||
"eslint-plugin-react": "^5.1.1",
|
"eslint-plugin-react": "^5.1.1",
|
||||||
"exports-loader": "^0.6.3",
|
"exports-loader": "^0.6.3",
|
||||||
|
"extract-text-webpack-plugin": "^1.0.1",
|
||||||
"express": "^4.13.4",
|
"express": "^4.13.4",
|
||||||
"file-loader": "^0.8.5",
|
"file-loader": "^0.8.5",
|
||||||
"immutable": "^3.7.6",
|
"immutable": "^3.7.6",
|
||||||
@ -53,8 +54,8 @@
|
|||||||
"react-lazy-load": "^3.0.3",
|
"react-lazy-load": "^3.0.3",
|
||||||
"react-pure-render": "^1.0.2",
|
"react-pure-render": "^1.0.2",
|
||||||
"react-redux": "^4.4.0",
|
"react-redux": "^4.4.0",
|
||||||
"react-router": "^2.0.0",
|
"react-router": "^2.5.1",
|
||||||
"react-router-redux": "^3.0.0",
|
"react-router-redux": "^4.0.5",
|
||||||
"redux": "^3.3.1",
|
"redux": "^3.3.1",
|
||||||
"redux-thunk": "^1.0.3",
|
"redux-thunk": "^1.0.3",
|
||||||
"style-loader": "^0.13.0",
|
"style-loader": "^0.13.0",
|
||||||
@ -65,14 +66,17 @@
|
|||||||
"whatwg-fetch": "^1.0.0"
|
"whatwg-fetch": "^1.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bricks.js": "^1.7.0",
|
||||||
"commonmark": "^0.24.0",
|
"commonmark": "^0.24.0",
|
||||||
"commonmark-react-renderer": "^4.1.2",
|
"commonmark-react-renderer": "^4.1.2",
|
||||||
"draft-js": "^0.7.0",
|
"draft-js": "^0.7.0",
|
||||||
"draft-js-export-markdown": "^0.2.0",
|
"draft-js-export-markdown": "^0.2.0",
|
||||||
"draft-js-import-markdown": "^0.1.6",
|
"draft-js-import-markdown": "^0.1.6",
|
||||||
"fuzzy": "^0.1.1",
|
"fuzzy": "^0.1.1",
|
||||||
|
"js-base64": "^2.1.9",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"localforage": "^1.4.2",
|
"localforage": "^1.4.2",
|
||||||
"lodash": "^4.13.1"
|
"lodash": "^4.13.1",
|
||||||
|
"pluralize": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export function loadConfig(config) {
|
|||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(configLoading());
|
dispatch(configLoading());
|
||||||
|
|
||||||
fetch('/config.yml').then((response) => {
|
fetch('config.yml').then((response) => {
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw `Failed to load config.yml (${response.status})`;
|
throw `Failed to load config.yml (${response.status})`;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { currentBackend } from '../backends/backend';
|
import { currentBackend } from '../backends/backend';
|
||||||
|
import { getMedia } from '../reducers';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Contant Declarations
|
* Contant Declarations
|
||||||
@ -11,7 +12,7 @@ export const ENTRIES_REQUEST = 'ENTRIES_REQUEST';
|
|||||||
export const ENTRIES_SUCCESS = 'ENTRIES_SUCCESS';
|
export const ENTRIES_SUCCESS = 'ENTRIES_SUCCESS';
|
||||||
export const ENTRIES_FAILURE = 'ENTRIES_FAILURE';
|
export const ENTRIES_FAILURE = 'ENTRIES_FAILURE';
|
||||||
|
|
||||||
export const DRAFT_CREATE = 'DRAFT_CREATE';
|
export const DRAFT_CREATE_FROM_ENTRY = 'DRAFT_CREATE_FROM_ENTRY';
|
||||||
export const DRAFT_DISCARD = 'DRAFT_DISCARD';
|
export const DRAFT_DISCARD = 'DRAFT_DISCARD';
|
||||||
export const DRAFT_CHANGE = 'DRAFT_CHANGE';
|
export const DRAFT_CHANGE = 'DRAFT_CHANGE';
|
||||||
|
|
||||||
@ -69,7 +70,7 @@ function entriesFailed(collection, error) {
|
|||||||
type: ENTRIES_FAILURE,
|
type: ENTRIES_FAILURE,
|
||||||
error: 'Failed to load entries',
|
error: 'Failed to load entries',
|
||||||
payload: error.toString(),
|
payload: error.toString(),
|
||||||
meta: {collection: collection.get('name')}
|
meta: { collection: collection.get('name') }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,12 +84,12 @@ function entryPersisting(collection, entry) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function entryPersisted(persistedEntry, persistedMediaFiles) {
|
function entryPersisted(collection, entry) {
|
||||||
return {
|
return {
|
||||||
type: ENTRY_PERSIST_SUCCESS,
|
type: ENTRY_PERSIST_SUCCESS,
|
||||||
payload: {
|
payload: {
|
||||||
persistedEntry: persistedEntry,
|
collection: collection,
|
||||||
persistedMediaFiles: persistedMediaFiles
|
entry: entry
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -104,9 +105,9 @@ function entryPersistFail(collection, entry, error) {
|
|||||||
/*
|
/*
|
||||||
* Exported simple Action Creators
|
* Exported simple Action Creators
|
||||||
*/
|
*/
|
||||||
export function createDraft(entry) {
|
export function createDraftFromEntry(entry) {
|
||||||
return {
|
return {
|
||||||
type: DRAFT_CREATE,
|
type: DRAFT_CREATE_FROM_ENTRY,
|
||||||
payload: entry
|
payload: entry
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -152,14 +153,16 @@ export function loadEntries(collection) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function persist(collection, entry, mediaFiles) {
|
export function persistEntry(collection, entry) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const backend = currentBackend(state.config);
|
const backend = currentBackend(state.config);
|
||||||
|
const MediaProxies = entry.get('mediaFiles').map(path => getMedia(state, path));
|
||||||
|
|
||||||
dispatch(entryPersisting(collection, entry));
|
dispatch(entryPersisting(collection, entry));
|
||||||
backend.persist(collection, entry, mediaFiles).then(
|
backend.persistEntry(collection, entry, MediaProxies.toJS()).then(
|
||||||
({persistedEntry, persistedMediaFiles}) => {
|
() => {
|
||||||
dispatch(entryPersisted(persistedEntry, persistedMediaFiles));
|
dispatch(entryPersisted(collection, entry));
|
||||||
},
|
},
|
||||||
(error) => dispatch(entryPersistFail(collection, entry, error))
|
(error) => dispatch(entryPersistFail(collection, entry, error))
|
||||||
);
|
);
|
||||||
|
@ -1,30 +1,29 @@
|
|||||||
import { browserHistory } from 'react-router';
|
import history from '../routing/history';
|
||||||
import { SEARCH } from '../containers/FindBar';
|
import { SEARCH } from '../containers/FindBar';
|
||||||
|
|
||||||
export const RUN_COMMAND = 'RUN_COMMAND';
|
export const RUN_COMMAND = 'RUN_COMMAND';
|
||||||
export const LIST_POSTS = 'LIST_POSTS';
|
export const SHOW_COLLECTION = 'SHOW_COLLECTION';
|
||||||
export const LIST_FAQ = 'LIST_FAQ';
|
export const CREATE_COLLECTION = 'CREATE_COLLECTION';
|
||||||
export const HELP = 'HELP';
|
export const HELP = 'HELP';
|
||||||
|
|
||||||
export function run(commandName, payload) {
|
export function run(commandName, payload) {
|
||||||
return { type: RUN_COMMAND, command: commandName, payload };
|
return { type: RUN_COMMAND, command: commandName, payload };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function runCommand(commandName, payload) {
|
export function runCommand(commandName, payload) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
switch (commandName) {
|
switch (commandName) {
|
||||||
case LIST_POSTS:
|
case SHOW_COLLECTION:
|
||||||
browserHistory.push('/collections/posts');
|
history.push(`/collections/${payload.collectionName}`);
|
||||||
break;
|
break;
|
||||||
case LIST_FAQ:
|
case CREATE_COLLECTION:
|
||||||
browserHistory.push('/collections/faq');
|
window.alert(`Create a new ${payload.collectionName} - not supported yet`);
|
||||||
break;
|
break;
|
||||||
case HELP:
|
case HELP:
|
||||||
window.alert('Find Bar Help (PLACEHOLDER)\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
window.alert('Find Bar Help (PLACEHOLDER)\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
||||||
break;
|
break;
|
||||||
case SEARCH:
|
case SEARCH:
|
||||||
browserHistory.push('/search');
|
history.push('/search');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
dispatch(run(commandName, payload));
|
dispatch(run(commandName, payload));
|
||||||
|
@ -2,9 +2,9 @@ export const ADD_MEDIA = 'ADD_MEDIA';
|
|||||||
export const REMOVE_MEDIA = 'REMOVE_MEDIA';
|
export const REMOVE_MEDIA = 'REMOVE_MEDIA';
|
||||||
|
|
||||||
export function addMedia(mediaProxy) {
|
export function addMedia(mediaProxy) {
|
||||||
return {type: ADD_MEDIA, payload: mediaProxy};
|
return { type: ADD_MEDIA, payload: mediaProxy };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeMedia(uri) {
|
export function removeMedia(path) {
|
||||||
return {type: REMOVE_MEDIA, payload: uri};
|
return { type: REMOVE_MEDIA, payload: path };
|
||||||
}
|
}
|
||||||
|
@ -67,19 +67,19 @@ class Backend {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
persist(collection, entryDraft) {
|
persistEntry(collection, entryDraft, MediaFiles) {
|
||||||
const entryData = entryDraft.getIn(['entry', 'data']).toObject();
|
const entryData = entryDraft.getIn(['entry', 'data']).toObject();
|
||||||
const entryObj = {
|
const entryObj = {
|
||||||
path: entryDraft.getIn(['entry', 'path']),
|
path: entryDraft.getIn(['entry', 'path']),
|
||||||
slug: entryDraft.getIn(['entry', 'slug']),
|
slug: entryDraft.getIn(['entry', 'slug']),
|
||||||
raw: this.entryToRaw(collection, entryData)
|
raw: this.entryToRaw(collection, entryData)
|
||||||
};
|
};
|
||||||
return this.implementation.persist(collection, entryObj, entryDraft.get('mediaFiles').toJS()).then(
|
|
||||||
(response) => ({
|
const commitMessage = (entryDraft.getIn(['entry', 'newRecord']) ? 'Created ' : 'Updated ') +
|
||||||
persistedEntry: this.entryWithFormat(collection)(response.persistedEntry),
|
collection.get('label') + ' “' +
|
||||||
persistedMediaFiles:response.persistedMediaFiles
|
entryDraft.getIn(['entry', 'data', 'title']) + '”';
|
||||||
})
|
|
||||||
);
|
return this.implementation.persistEntry(collection, entryObj, MediaFiles, { commitMessage });
|
||||||
}
|
}
|
||||||
|
|
||||||
entryToRaw(collection, entry) {
|
entryToRaw(collection, entry) {
|
||||||
|
@ -14,9 +14,14 @@ export default class AuthenticationPage extends React.Component {
|
|||||||
|
|
||||||
handleLogin(e) {
|
handleLogin(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
let auth;
|
||||||
|
if (document.location.host.split(':')[0] === 'localhost') {
|
||||||
|
auth = new Authenticator({ site_id: 'cms.netlify.com' });
|
||||||
|
} else {
|
||||||
|
auth = new Authenticator();
|
||||||
|
}
|
||||||
|
|
||||||
const auth = new Authenticator({site_id: 'cms.netlify.com'});
|
auth.authenticate({provider: 'github', scope: 'repo'}, (err, data) => {
|
||||||
auth.authenticate({provider: 'github', scope: 'user'}, (err, data) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
this.setState({loginError: err.toString()});
|
this.setState({loginError: err.toString()});
|
||||||
return;
|
return;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import LocalForage from 'localforage';
|
import LocalForage from 'localforage';
|
||||||
|
import MediaProxy from '../../valueObjects/MediaProxy';
|
||||||
import AuthenticationPage from './AuthenticationPage';
|
import AuthenticationPage from './AuthenticationPage';
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
|
||||||
const API_ROOT = 'https://api.github.com';
|
const API_ROOT = 'https://api.github.com';
|
||||||
|
|
||||||
@ -8,7 +10,7 @@ class API {
|
|||||||
this.token = token;
|
this.token = token;
|
||||||
this.repo = repo;
|
this.repo = repo;
|
||||||
this.branch = branch;
|
this.branch = branch;
|
||||||
this.baseURL = API_ROOT + `/repos/${this.repo}`;
|
this.repoURL = `/repos/${this.repo}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
user() {
|
user() {
|
||||||
@ -20,9 +22,9 @@ class API {
|
|||||||
return cache.then((cached) => {
|
return cache.then((cached) => {
|
||||||
if (cached) { return cached; }
|
if (cached) { return cached; }
|
||||||
|
|
||||||
return this.request(`/contents/${path}`, {
|
return this.request(`${this.repoURL}/contents/${path}`, {
|
||||||
headers: {Accept: 'application/vnd.github.VERSION.raw'},
|
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||||
data: {ref: this.branch},
|
body: { ref: this.branch },
|
||||||
cache: false
|
cache: false
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (sha) {
|
if (sha) {
|
||||||
@ -35,11 +37,48 @@ class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listFiles(path) {
|
listFiles(path) {
|
||||||
return this.request(`/contents/${path}`, {
|
return this.request(`${this.repoURL}/contents/${path}`, {
|
||||||
data: {ref: this.branch}
|
body: { ref: this.branch }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
persistFiles(collection, entry, mediaFiles, options) {
|
||||||
|
let filename, part, parts, subtree;
|
||||||
|
const fileTree = {};
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
mediaFiles.concat(entry).forEach((file) => {
|
||||||
|
if (file.uploaded) { return; }
|
||||||
|
files.push(this.uploadBlob(file));
|
||||||
|
parts = file.path.split('/').filter((part) => part);
|
||||||
|
filename = parts.pop();
|
||||||
|
subtree = fileTree;
|
||||||
|
while (part = parts.shift()) {
|
||||||
|
subtree[part] = subtree[part] || {};
|
||||||
|
subtree = subtree[part];
|
||||||
|
}
|
||||||
|
subtree[filename] = file;
|
||||||
|
file.file = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(files)
|
||||||
|
.then(() => this.getBranch())
|
||||||
|
.then((branchData) => {
|
||||||
|
return this.updateTree(branchData.commit.sha, '/', fileTree);
|
||||||
|
})
|
||||||
|
.then((changeTree) => {
|
||||||
|
return this.request(`${this.repoURL}/git/commits`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ message: options.commitMessage, tree: changeTree.sha, parents: [changeTree.parentSha] })
|
||||||
|
});
|
||||||
|
}).then((response) => {
|
||||||
|
return this.request(`${this.repoURL}/git/refs/heads/${this.branch}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ sha: response.sha })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
requestHeaders(headers = {}) {
|
requestHeaders(headers = {}) {
|
||||||
return {
|
return {
|
||||||
Authorization: `token ${this.token}`,
|
Authorization: `token ${this.token}`,
|
||||||
@ -60,7 +99,7 @@ class API {
|
|||||||
|
|
||||||
request(path, options = {}) {
|
request(path, options = {}) {
|
||||||
const headers = this.requestHeaders(options.headers || {});
|
const headers = this.requestHeaders(options.headers || {});
|
||||||
return fetch(this.baseURL + path, {...options, headers: headers}).then((response) => {
|
return fetch(API_ROOT + path, { ...options, headers: headers }).then((response) => {
|
||||||
if (response.headers.get('Content-Type').match(/json/)) {
|
if (response.headers.get('Content-Type').match(/json/)) {
|
||||||
return this.parseJsonResponse(response);
|
return this.parseJsonResponse(response);
|
||||||
}
|
}
|
||||||
@ -68,6 +107,78 @@ class API {
|
|||||||
return response.text();
|
return response.text();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBranch() {
|
||||||
|
return this.request(`${this.repoURL}/branches/${this.branch}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTree(sha) {
|
||||||
|
return sha ? this.request(`${this.repoURL}/git/trees/${sha}`) : Promise.resolve({ tree: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
toBase64(str) {
|
||||||
|
return Promise.resolve(
|
||||||
|
Base64.encode(str)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadBlob(item) {
|
||||||
|
const content = item instanceof MediaProxy ? item.toBase64() : this.toBase64(item.raw);
|
||||||
|
|
||||||
|
return content.then((contentBase64) => {
|
||||||
|
return this.request(`${this.repoURL}/git/blobs`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: contentBase64,
|
||||||
|
encoding: 'base64'
|
||||||
|
})
|
||||||
|
}).then((response) => {
|
||||||
|
item.sha = response.sha;
|
||||||
|
item.uploaded = true;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTree(sha, path, fileTree) {
|
||||||
|
return this.getTree(sha)
|
||||||
|
.then((tree) => {
|
||||||
|
var obj, filename, fileOrDir;
|
||||||
|
var updates = [];
|
||||||
|
var added = {};
|
||||||
|
|
||||||
|
for (var i = 0, len = tree.tree.length; i < len; i++) {
|
||||||
|
obj = tree.tree[i];
|
||||||
|
if (fileOrDir = fileTree[obj.path]) {
|
||||||
|
added[obj.path] = true;
|
||||||
|
if (fileOrDir.file) {
|
||||||
|
updates.push({ path: obj.path, mode: obj.mode, type: obj.type, sha: fileOrDir.sha });
|
||||||
|
} else {
|
||||||
|
updates.push(this.updateTree(obj.sha, obj.path, fileOrDir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (filename in fileTree) {
|
||||||
|
fileOrDir = fileTree[filename];
|
||||||
|
if (added[filename]) { continue; }
|
||||||
|
updates.push(
|
||||||
|
fileOrDir.file ?
|
||||||
|
{ path: filename, mode: '100644', type: 'blob', sha: fileOrDir.sha } :
|
||||||
|
this.updateTree(null, filename, fileOrDir)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.all(updates)
|
||||||
|
.then((updates) => {
|
||||||
|
return this.request(`${this.repoURL}/git/trees`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ base_tree: sha, tree: updates })
|
||||||
|
});
|
||||||
|
}).then((response) => {
|
||||||
|
return { path: path, mode: '040000', type: 'tree', sha: response.sha, parentSha: sha };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GitHub {
|
export default class GitHub {
|
||||||
@ -115,4 +226,8 @@ export default class GitHub {
|
|||||||
response.entries.filter((entry) => entry.slug === slug)[0]
|
response.entries.filter((entry) => entry.slug === slug)[0]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
persistEntry(collection, entry, mediaFiles = [], options = {}) {
|
||||||
|
return this.api.persistFiles(collection, entry, mediaFiles, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ export default class TestRepo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authenticate(state) {
|
authenticate(state) {
|
||||||
return Promise.resolve({email: state.email});
|
return Promise.resolve({ email: state.email });
|
||||||
}
|
}
|
||||||
|
|
||||||
entries(collection) {
|
entries(collection) {
|
||||||
@ -48,10 +48,11 @@ export default class TestRepo {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
persist(collection, entry, mediaFiles = []) {
|
persistEntry(collection, entry, mediaFiles = []) {
|
||||||
const folder = collection.get('folder');
|
const folder = entry.path.substring(0, entry.path.lastIndexOf('/'));
|
||||||
const fileName = entry.path.substring(entry.path.lastIndexOf('/') + 1);
|
const fileName = entry.path.substring(entry.path.lastIndexOf('/') + 1);
|
||||||
window.repoFiles[folder][fileName]['content'] = entry.raw;
|
window.repoFiles[folder][fileName]['content'] = entry.raw;
|
||||||
return Promise.resolve({persistedEntry:entry, persistedMediaFiles:[]});
|
mediaFiles.forEach(media => media.uploaded = true);
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
src/components/Cards.js
Normal file
11
src/components/Cards.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import UnknownCard from './Cards/UnknownCard';
|
||||||
|
import ImageCard from './Cards/ImageCard';
|
||||||
|
import AlltypeCard from './Cards/AlltypeCard';
|
||||||
|
|
||||||
|
const Cards = {
|
||||||
|
_unknown: UnknownCard,
|
||||||
|
image: ImageCard,
|
||||||
|
alltype: AlltypeCard
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cards;
|
5
src/components/Cards/AlltypeCard.css
Normal file
5
src/components/Cards/AlltypeCard.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.cardContent {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
81
src/components/Cards/AlltypeCard.js
Normal file
81
src/components/Cards/AlltypeCard.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Card } from '../UI';
|
||||||
|
import ScaledLine from './ScaledLine';
|
||||||
|
import styles from './AlltypeCard.css';
|
||||||
|
|
||||||
|
export default class AlltypeCard extends React.Component {
|
||||||
|
|
||||||
|
// Based on the Slabtype Algorithm by Erik Loyer
|
||||||
|
// http://erikloyer.com/index.php/blog/the_slabtype_algorithm_part_1_background/
|
||||||
|
renderInscription(inscription) {
|
||||||
|
|
||||||
|
const idealCharPerLine = 22;
|
||||||
|
|
||||||
|
// segment the text into lines
|
||||||
|
const words = inscription.split(' ');
|
||||||
|
let preText, postText, finalText;
|
||||||
|
let preDiff, postDiff;
|
||||||
|
let wordIndex = 0;
|
||||||
|
const lineText = [];
|
||||||
|
|
||||||
|
// while we still have words left, build the next line
|
||||||
|
while (wordIndex < words.length) {
|
||||||
|
postText = '';
|
||||||
|
|
||||||
|
// build two strings (preText and postText) word by word, with one
|
||||||
|
// string always one word behind the other, until
|
||||||
|
// the length of one string is less than the ideal number of characters
|
||||||
|
// per line, while the length of the other is greater than that ideal
|
||||||
|
while (postText.length < idealCharPerLine) {
|
||||||
|
preText = postText;
|
||||||
|
postText += words[wordIndex] + ' ';
|
||||||
|
wordIndex++;
|
||||||
|
if (wordIndex >= words.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate the character difference between the two strings and the
|
||||||
|
// ideal number of characters per line
|
||||||
|
preDiff = idealCharPerLine - preText.length;
|
||||||
|
postDiff = postText.length - idealCharPerLine;
|
||||||
|
|
||||||
|
// if the smaller string is closer to the length of the ideal than
|
||||||
|
// the longer string, and doesn’t contain just a single space, then
|
||||||
|
// use that one for the line
|
||||||
|
if ((preDiff < postDiff) && (preText.length > 2)) {
|
||||||
|
finalText = preText;
|
||||||
|
wordIndex--;
|
||||||
|
|
||||||
|
// otherwise, use the longer string for the line
|
||||||
|
} else {
|
||||||
|
finalText = postText;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineText.push(finalText.substr(0, finalText.length - 1));
|
||||||
|
}
|
||||||
|
return lineText.map(text => (
|
||||||
|
<ScaledLine key={text.trim().replace(/[^a-z0-9]+/gi, '-')} toWidth={216}>
|
||||||
|
{text}
|
||||||
|
</ScaledLine>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { onClick, text } = this.props;
|
||||||
|
return (
|
||||||
|
<Card onClick={onClick}>
|
||||||
|
<div className={styles.cardContent}>{this.renderInscription(text)}</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlltypeCard.propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
text: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
AlltypeCard.defaultProps = {
|
||||||
|
onClick: function() {},
|
||||||
|
};
|
18
src/components/Cards/ImageCard.css
Normal file
18
src/components/Cards/ImageCard.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.root {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root h1 {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root h2 {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root p {
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
31
src/components/Cards/ImageCard.js
Normal file
31
src/components/Cards/ImageCard.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Card } from '../UI';
|
||||||
|
import styles from './ImageCard.css';
|
||||||
|
|
||||||
|
export default class ImageCard extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { onClick, onImageLoaded, image, text, description } = this.props;
|
||||||
|
return (
|
||||||
|
<Card onClick={onClick} className={styles.root}>
|
||||||
|
<img src={image} onLoad={onImageLoaded} />
|
||||||
|
<h1>{text}</h1>
|
||||||
|
|
||||||
|
{description ? <p>{description}</p> : null}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageCard.propTypes = {
|
||||||
|
image: PropTypes.string,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onImageLoaded: PropTypes.func,
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
ImageCard.defaultProps = {
|
||||||
|
onClick: function() {},
|
||||||
|
onImageLoaded: function() {}
|
||||||
|
};
|
39
src/components/Cards/ScaledLine.js
Normal file
39
src/components/Cards/ScaledLine.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
|
||||||
|
export default class ScaledLine extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._content = null;
|
||||||
|
this.state = {
|
||||||
|
ratio: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const actualContent = this._content.children[0];
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
ratio: this.props.toWidth / actualContent.offsetWidth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { ratio } = this.state;
|
||||||
|
const { children } = this.props;
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
fontSize: ratio.toFixed(3) + 'em'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={(c) => this._content = c} style={styles}>
|
||||||
|
<span>{children}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaledLine.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
toWidth: PropTypes.number.isRequired
|
||||||
|
};
|
15
src/components/Cards/UnknownCard.js
Normal file
15
src/components/Cards/UnknownCard.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { Card } from '../UI';
|
||||||
|
|
||||||
|
export default function UnknownCard({ collection }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<p>No card of type “{collection.getIn(['card', 'type'])}”.</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UnknownCard.propTypes = {
|
||||||
|
collection: ImmutablePropTypes.map,
|
||||||
|
};
|
@ -1,18 +1,91 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { Link } from 'react-router';
|
import Bricks from 'bricks.js';
|
||||||
|
import history from '../routing/history';
|
||||||
|
import Cards from './Cards';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
export default function EntryListing({ collection, entries }) {
|
export default class EntryListing extends React.Component {
|
||||||
const name = collection.get('name');
|
constructor(props) {
|
||||||
return <div>
|
super(props);
|
||||||
<h2>Listing entries!</h2>
|
this.bricksInstance = null;
|
||||||
{entries.map((entry) => {
|
|
||||||
const path = `/collections/${name}/entries/${entry.get('slug')}`;
|
this.bricksConfig = {
|
||||||
return <Link key={entry.get('slug')} to={path}>
|
packed: 'data-packed',
|
||||||
<h3>{entry.getIn(['data', 'title'])}</h3>
|
sizes: [
|
||||||
</Link>;
|
{ columns: 1, gutter: 15 },
|
||||||
})}
|
{ mq: '495px', columns: 2, gutter: 15 },
|
||||||
</div>;
|
{ mq: '750px', columns: 3, gutter: 15 },
|
||||||
|
{ mq: '1005px', columns: 4, gutter: 15 },
|
||||||
|
{ mq: '1260px', columns: 5, gutter: 15 },
|
||||||
|
{ mq: '1515px', columns: 6, gutter: 15 },
|
||||||
|
{ mq: '1770px', columns: 7, gutter: 15 },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateBricks = _.throttle(this.updateBricks.bind(this), 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.bricksInstance = Bricks({
|
||||||
|
container: this._entries,
|
||||||
|
packed: this.bricksConfig.packed,
|
||||||
|
sizes: this.bricksConfig.sizes
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bricksInstance.resize(true);
|
||||||
|
|
||||||
|
if (this.props.entries && this.props.entries.size > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.bricksInstance.pack();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if ((prevProps.entries === undefined || prevProps.entries.size === 0) && this.props.entries.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bricksInstance.pack();
|
||||||
|
}
|
||||||
|
|
||||||
|
componengWillUnmount() {
|
||||||
|
this.bricksInstance.resize(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBricks() {
|
||||||
|
this.bricksInstance.pack();
|
||||||
|
}
|
||||||
|
|
||||||
|
cardFor(collection, entry, link) {
|
||||||
|
//const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props;
|
||||||
|
const card = Cards[collection.getIn(['card', 'type'])] || Cards._unknown;
|
||||||
|
return React.createElement(card, {
|
||||||
|
key: entry.get('slug'),
|
||||||
|
collection: collection,
|
||||||
|
onClick: history.push.bind(this, link),
|
||||||
|
onImageLoaded: this.updateBricks,
|
||||||
|
text: entry.getIn(['data', collection.getIn(['card', 'text'])]),
|
||||||
|
description: entry.getIn(['data', collection.getIn(['card', 'description'])]),
|
||||||
|
image: entry.getIn(['data', collection.getIn(['card', 'image'])]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { collection, entries } = this.props;
|
||||||
|
const name = collection.get('name');
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<h1>Listing {name}</h1>
|
||||||
|
<div ref={(c) => this._entries = c}>
|
||||||
|
{entries.map((entry) => {
|
||||||
|
const path = `/collections/${name}/entries/${entry.get('slug')}`;
|
||||||
|
return this.cardFor(collection, entry, path);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EntryListing.propTypes = {
|
EntryListing.propTypes = {
|
||||||
|
@ -4,10 +4,12 @@
|
|||||||
composes: rounded from "../theme.css";
|
composes: rounded from "../theme.css";
|
||||||
composes: depth from "../theme.css";
|
composes: depth from "../theme.css";
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card > *:not(iframe, video, img, header, footer) {
|
.card > *:not(iframe, video, img, header, footer) {
|
||||||
margin: 0 10px;
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card > *:not(iframe, video, img, header, footer):first-child {
|
.card > *:not(iframe, video, img, header, footer):first-child {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styles from './Card.css';
|
import styles from './Card.css';
|
||||||
|
|
||||||
export default function Card({ style, className = '', children }) {
|
export default function Card({ style, className = '', onClick, children }) {
|
||||||
return <div className={`${styles.card} ${className}`} style={style}>{children}</div>;
|
return <div className={`${styles.card} ${className}`} style={style} onClick={onClick}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
:root {
|
:root {
|
||||||
--defaultColor: #333;
|
--defaultColor: #333;
|
||||||
--backgroundColor: #fff;
|
--backgroundColor: #fff;
|
||||||
--shadowColor: rgba(0, 0, 0, 0.25);
|
--shadowColor: rgba(0, 0, 0, 0.117647);
|
||||||
--successColor: #1c7;
|
--successColor: #1c7;
|
||||||
--warningColor: #fa0;
|
--warningColor: #fa0;
|
||||||
--errorColor: #f52;
|
--errorColor: #f52;
|
||||||
@ -12,15 +12,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin: 10px;
|
|
||||||
color: var(--defaultColor);
|
color: var(--defaultColor);
|
||||||
background-color: var(--backgroundColor);
|
background-color: var(--backgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded {
|
.rounded {
|
||||||
border-radius: 6px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.depth {
|
.depth {
|
||||||
box-shadow: 0px 1px 2px 0px var(--shadowColor);
|
box-shadow: var(--shadowColor) 0px 1px 6px, var(--shadowColor) 0px 1px 4px;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import UnknownControl from './widgets/UnknownControl';
|
import UnknownControl from './Widgets/UnknownControl';
|
||||||
import UnknownPreview from './widgets/UnknownPreview';
|
import UnknownPreview from './Widgets/UnknownPreview';
|
||||||
import StringControl from './widgets/StringControl';
|
import StringControl from './Widgets/StringControl';
|
||||||
import StringPreview from './widgets/StringPreview';
|
import StringPreview from './Widgets/StringPreview';
|
||||||
import MarkdownControl from './widgets/MarkdownControl';
|
import MarkdownControl from './Widgets/MarkdownControl';
|
||||||
import MarkdownPreview from './widgets/MarkdownPreview';
|
import MarkdownPreview from './Widgets/MarkdownPreview';
|
||||||
import ImageControl from './widgets/ImageControl';
|
import ImageControl from './Widgets/ImageControl';
|
||||||
import ImagePreview from './widgets/ImagePreview';
|
import ImagePreview from './Widgets/ImagePreview';
|
||||||
|
|
||||||
|
|
||||||
const Widgets = {
|
const Widgets = {
|
||||||
|
@ -53,7 +53,7 @@ export default class ImageControl extends React.Component {
|
|||||||
if (file) {
|
if (file) {
|
||||||
const mediaProxy = new MediaProxy(file.name, file);
|
const mediaProxy = new MediaProxy(file.name, file);
|
||||||
this.props.onAddMedia(mediaProxy);
|
this.props.onAddMedia(mediaProxy);
|
||||||
this.props.onChange(mediaProxy.uri);
|
this.props.onChange(mediaProxy.path);
|
||||||
} else {
|
} else {
|
||||||
this.props.onChange(null);
|
this.props.onChange(null);
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@ export default class ImageControl extends React.Component {
|
|||||||
renderImageName() {
|
renderImageName() {
|
||||||
if (!this.props.value) return null;
|
if (!this.props.value) return null;
|
||||||
if (this.value instanceof MediaProxy) {
|
if (this.value instanceof MediaProxy) {
|
||||||
return truncateMiddle(this.props.value.uri, MAX_DISPLAY_LENGTH);
|
return truncateMiddle(this.props.value.path, MAX_DISPLAY_LENGTH);
|
||||||
} else {
|
} else {
|
||||||
return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH);
|
return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { Card } from '../UI';
|
|||||||
import { storiesOf } from '@kadira/storybook';
|
import { storiesOf } from '@kadira/storybook';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
fixedWidth: { width: 280 },
|
|
||||||
footer: {
|
footer: {
|
||||||
color: '#aaa',
|
color: '#aaa',
|
||||||
backgroundColor: '#555',
|
backgroundColor: '#555',
|
||||||
@ -11,40 +10,33 @@ const styles = {
|
|||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
padding: 10
|
padding: 10
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
storiesOf('Card', module)
|
storiesOf('Card', module)
|
||||||
.add('Default View', () => (
|
.add('Default View', () => (
|
||||||
<div style={styles.fixedWidth}>
|
<Card>
|
||||||
<Card>
|
<h1>A Card</h1>
|
||||||
<h1>A Card</h1>
|
<h2>Subtitle</h2>
|
||||||
<h2>Subtitle</h2>
|
<p>
|
||||||
<p>
|
Margins are applied to all elements inside a card. <br/>
|
||||||
Margins are applied to all elements inside a card. <br/>
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. lobortis vel. Nulla porttitor enim at tellus eget
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. lobortis vel. Nulla porttitor enim at tellus eget
|
malesuada eleifend. Nunc tellus turpis, tincidunt sed felis facilisis, lacinia condimentum quam. Cras quis
|
||||||
malesuada eleifend. Nunc tellus turpis, tincidunt sed felis facilisis, lacinia condimentum quam. Cras quis
|
tortor fermentum, aliquam tortor eu, consequat ligula. Nulla eget nulla act odio varius ullamcorper turpis.
|
||||||
tortor fermentum, aliquam tortor eu, consequat ligula. Nulla eget nulla act odio varius ullamcorper turpis.
|
In consequat egestas nulla condimentum faucibus. Donec scelerisque convallis est nec fringila. Suspendisse
|
||||||
In consequat egestas nulla condimentum faucibus. Donec scelerisque convallis est nec fringila. Suspendisse
|
non lorem non erat congue consequat.
|
||||||
non lorem non erat congue consequat.
|
</p>
|
||||||
</p>
|
</Card>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)).add('Full width content', () => (
|
)).add('Full width content', () => (
|
||||||
<div style={styles.fixedWidth}>
|
<Card>
|
||||||
<Card>
|
<img src="https://i.ytimg.com/vi/tntOCGkgt98/maxresdefault.jpg" />
|
||||||
<img src="https://i.ytimg.com/vi/tntOCGkgt98/maxresdefault.jpg" />
|
<h1>Card & cat</h1>
|
||||||
<h1>Card & cat</h1>
|
<p>Media Elements such as video, img (and iFrame for embeds) don't have margin</p>
|
||||||
<p>Media Elements such as video, img (and iFrame for embeds) don't have margin</p>
|
</Card>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)).add('Footer', () => (
|
)).add('Footer', () => (
|
||||||
<div style={styles.fixedWidth}>
|
<Card>
|
||||||
<Card>
|
<img src="http://www.top13.net/wp-content/uploads/2015/10/perfectly-timed-funny-cat-pictures-5.jpg" />
|
||||||
<img src="http://www.top13.net/wp-content/uploads/2015/10/perfectly-timed-funny-cat-pictures-5.jpg" />
|
<h1>Now with footer.</h1>
|
||||||
<h1>Now with footer.</h1>
|
<p>header and footer elements are also not subject to margin</p>
|
||||||
<p>header and footer elements are also not subject to margin</p>
|
<footer style={styles.footer}>© Thousand Cats Corp</footer>
|
||||||
<footer style={styles.footer}>© Thousand Cats Corp</footer>
|
</Card>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
))
|
))
|
||||||
|
43
src/containers/App.css
Normal file
43
src/containers/App.css
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.alignable {
|
||||||
|
margin: 0px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 749px) and (min-width: 495px) {
|
||||||
|
.alignable {
|
||||||
|
width: 495px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1004px) and (min-width: 750px) {
|
||||||
|
.alignable {
|
||||||
|
width: 750px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1259px) and (min-width: 1005px) {
|
||||||
|
.alignable {
|
||||||
|
width: 1005px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1514px) and (min-width: 1260px) {
|
||||||
|
.alignable {
|
||||||
|
width: 1260px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1769px) and (min-width: 1515px) {
|
||||||
|
.alignable {
|
||||||
|
width: 1515px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1770px) {
|
||||||
|
.alignable {
|
||||||
|
width: 1770px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
@ -3,8 +3,10 @@ import { connect } from 'react-redux';
|
|||||||
import { loadConfig } from '../actions/config';
|
import { loadConfig } from '../actions/config';
|
||||||
import { loginUser } from '../actions/auth';
|
import { loginUser } from '../actions/auth';
|
||||||
import { currentBackend } from '../backends/backend';
|
import { currentBackend } from '../backends/backend';
|
||||||
import { LIST_POSTS, LIST_FAQ, HELP, MORE_COMMANDS } from '../actions/findbar';
|
import { SHOW_COLLECTION, CREATE_COLLECTION, HELP } from '../actions/findbar';
|
||||||
import FindBar from './FindBar';
|
import FindBar from './FindBar';
|
||||||
|
import styles from './App.css';
|
||||||
|
import pluralize from 'pluralize';
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -49,6 +51,37 @@ class App extends React.Component {
|
|||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateFindBarCommands() {
|
||||||
|
// Generate command list
|
||||||
|
const commands = [];
|
||||||
|
const defaultCommands = [];
|
||||||
|
|
||||||
|
this.props.collections.forEach(collection => {
|
||||||
|
commands.push({
|
||||||
|
id: `show_${collection.get('name')}`,
|
||||||
|
pattern: `Show ${pluralize(collection.get('label'))}`,
|
||||||
|
type: SHOW_COLLECTION,
|
||||||
|
payload: { collectionName:collection.get('name') }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (defaultCommands.length < 5) defaultCommands.push(`show_${collection.get('name')}`);
|
||||||
|
|
||||||
|
if (collection.get('create') === true) {
|
||||||
|
commands.push({
|
||||||
|
id: `create_${collection.get('name')}`,
|
||||||
|
pattern: `Create new ${pluralize(collection.get('label'), 1)}(:itemName as ${pluralize(collection.get('label'), 1)} Name)`,
|
||||||
|
type: CREATE_COLLECTION,
|
||||||
|
payload: { collectionName:collection.get('name') }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
commands.push({ id: HELP, type: HELP, pattern: 'Help' });
|
||||||
|
defaultCommands.push(HELP);
|
||||||
|
|
||||||
|
return { commands, defaultCommands };
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { user, config, children } = this.props;
|
const { user, config, children } = this.props;
|
||||||
|
|
||||||
@ -68,24 +101,31 @@ class App extends React.Component {
|
|||||||
return this.authenticating();
|
return this.authenticating();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { commands, defaultCommands } = this.generateFindBarCommands();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FindBar commands={[
|
<header>
|
||||||
{ id: LIST_POSTS, pattern: 'List Posts' },
|
<div className={styles.alignable}>
|
||||||
{ id: LIST_FAQ, pattern: 'List FAQs' },
|
<FindBar
|
||||||
{ id: HELP, pattern: 'Help' },
|
commands={commands}
|
||||||
]} />
|
defaultCommands={defaultCommands}
|
||||||
{children}
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className={`${styles.alignable} ${styles.main}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
const { auth, config } = state;
|
const { auth, config, collections } = state;
|
||||||
const user = auth && auth.get('user');
|
const user = auth && auth.get('user');
|
||||||
|
|
||||||
return { auth, config, user };
|
return { auth, config, collections, user };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(App);
|
export default connect(mapStateToProps)(App);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { Link } from 'react-router';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { loadEntries } from '../actions/entries';
|
import { loadEntries } from '../actions/entries';
|
||||||
import { selectEntries } from '../reducers';
|
import { selectEntries } from '../reducers';
|
||||||
@ -29,17 +28,7 @@ class DashboardPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<h1>Dashboard</h1>
|
{entries ? <EntryListing collection={collection} entries={entries}/> : 'Loading entries...'}
|
||||||
<div>
|
|
||||||
{collections.map((collection) => (
|
|
||||||
<div key={collection.get('name')}>
|
|
||||||
<Link to={`/collections/${collection.get('name')}`}>{collection.get('name')}</Link>
|
|
||||||
</div>
|
|
||||||
)).toArray()}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{entries ? <EntryListing collection={collection} entries={entries}/> : 'Loading entries...'}
|
|
||||||
</div>
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
loadEntry,
|
loadEntry,
|
||||||
createDraft,
|
createDraftFromEntry,
|
||||||
discardDraft,
|
discardDraft,
|
||||||
changeDraft,
|
changeDraft,
|
||||||
persist
|
persistEntry
|
||||||
} from '../actions/entries';
|
} from '../actions/entries';
|
||||||
import { addMedia, removeMedia } from '../actions/media';
|
import { addMedia, removeMedia } from '../actions/media';
|
||||||
import { selectEntry, getMedia } from '../reducers';
|
import { selectEntry, getMedia } from '../reducers';
|
||||||
@ -16,18 +16,18 @@ class EntryPage extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.props.loadEntry(props.collection, props.slug);
|
this.props.loadEntry(props.collection, props.slug);
|
||||||
this.handlePersist = this.handlePersist.bind(this);
|
this.handlePersistEntry = this.handlePersistEntry.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.entry) {
|
if (this.props.entry) {
|
||||||
this.props.createDraft(this.props.entry);
|
this.props.createDraftFromEntry(this.props.entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (this.props.entry !== nextProps.entry && !nextProps.entry.get('isFetching')) {
|
if (this.props.entry !== nextProps.entry && !nextProps.entry.get('isFetching')) {
|
||||||
this.props.createDraft(nextProps.entry);
|
this.props.createDraftFromEntry(nextProps.entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,8 +35,8 @@ class EntryPage extends React.Component {
|
|||||||
this.props.discardDraft();
|
this.props.discardDraft();
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePersist() {
|
handlePersistEntry() {
|
||||||
this.props.persist(this.props.collection, this.props.entryDraft);
|
this.props.persistEntry(this.props.collection, this.props.entryDraft);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -56,7 +56,7 @@ class EntryPage extends React.Component {
|
|||||||
onChange={changeDraft}
|
onChange={changeDraft}
|
||||||
onAddMedia={addMedia}
|
onAddMedia={addMedia}
|
||||||
onRemoveMedia={removeMedia}
|
onRemoveMedia={removeMedia}
|
||||||
onPersist={this.handlePersist}
|
onPersist={this.handlePersistEntry}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -67,12 +67,12 @@ EntryPage.propTypes = {
|
|||||||
boundGetMedia: PropTypes.func.isRequired,
|
boundGetMedia: PropTypes.func.isRequired,
|
||||||
changeDraft: PropTypes.func.isRequired,
|
changeDraft: PropTypes.func.isRequired,
|
||||||
collection: ImmutablePropTypes.map.isRequired,
|
collection: ImmutablePropTypes.map.isRequired,
|
||||||
createDraft: PropTypes.func.isRequired,
|
createDraftFromEntry: PropTypes.func.isRequired,
|
||||||
discardDraft: PropTypes.func.isRequired,
|
discardDraft: PropTypes.func.isRequired,
|
||||||
entry: ImmutablePropTypes.map.isRequired,
|
entry: ImmutablePropTypes.map.isRequired,
|
||||||
entryDraft: ImmutablePropTypes.map.isRequired,
|
entryDraft: ImmutablePropTypes.map.isRequired,
|
||||||
loadEntry: PropTypes.func.isRequired,
|
loadEntry: PropTypes.func.isRequired,
|
||||||
persist: PropTypes.func.isRequired,
|
persistEntry: PropTypes.func.isRequired,
|
||||||
removeMedia: PropTypes.func.isRequired,
|
removeMedia: PropTypes.func.isRequired,
|
||||||
slug: PropTypes.string.isRequired,
|
slug: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
@ -93,8 +93,8 @@ export default connect(
|
|||||||
addMedia,
|
addMedia,
|
||||||
removeMedia,
|
removeMedia,
|
||||||
loadEntry,
|
loadEntry,
|
||||||
createDraft,
|
createDraftFromEntry,
|
||||||
discardDraft,
|
discardDraft,
|
||||||
persist
|
persistEntry
|
||||||
}
|
}
|
||||||
)(EntryPage);
|
)(EntryPage);
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
background-color: var(--backgroundColor);
|
background-color: var(--backgroundColor);
|
||||||
padding: 1px 0;
|
padding: 1px 0;
|
||||||
|
margin: 4px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputArea {
|
.inputArea {
|
||||||
display: table;
|
display: table;
|
||||||
width: calc(100% - 10px);
|
width: calc(100% - 10px);
|
||||||
|
@ -124,8 +124,11 @@ class FindBar extends Component {
|
|||||||
}, () => {
|
}, () => {
|
||||||
this._input.blur();
|
this._input.blur();
|
||||||
});
|
});
|
||||||
const payload = paramName ? { [paramName]: enteredParamValue } : null;
|
const payload = command.payload || {};
|
||||||
this.props.dispatch(runCommand(command.id, payload));
|
if (paramName) {
|
||||||
|
payload[paramName] = enteredParamValue;
|
||||||
|
}
|
||||||
|
this.props.dispatch(runCommand(command.type, payload));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,6 +361,7 @@ class FindBar extends Component {
|
|||||||
FindBar.propTypes = {
|
FindBar.propTypes = {
|
||||||
commands: PropTypes.arrayOf(PropTypes.shape({
|
commands: PropTypes.arrayOf(PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
pattern: PropTypes.string.isRequired
|
pattern: PropTypes.string.isRequired
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
defaultCommands: PropTypes.arrayOf(PropTypes.string),
|
defaultCommands: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
@ -19,7 +19,7 @@ const ImageType = new yaml.Type('image', {
|
|||||||
kind: 'scalar',
|
kind: 'scalar',
|
||||||
instanceOf: MediaProxy,
|
instanceOf: MediaProxy,
|
||||||
represent: function(value) {
|
represent: function(value) {
|
||||||
return `${value.uri}`;
|
return `${value.path}`;
|
||||||
},
|
},
|
||||||
resolve: function(value) {
|
resolve: function(value) {
|
||||||
if (value === null) return false;
|
if (value === null) return false;
|
||||||
|
@ -2,6 +2,7 @@ html {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
*, *:before, *:after {
|
*, *:before, *:after {
|
||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
@ -10,13 +11,23 @@ html {
|
|||||||
body {
|
body {
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: 'Roboto', sans-serif;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
background-color: #fafafa;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.22);
|
||||||
|
height: 54px;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global #root, :global #root > * {
|
:global #root, :global #root > * {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6, p, blockquote, figure, dl, ol, ul {
|
h1, h2, h3, h4, h5, h6, p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
11
src/index.js
11
src/index.js
@ -1,19 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { Router } from 'react-router';
|
||||||
import configureStore from './store/configureStore';
|
import configureStore from './store/configureStore';
|
||||||
import Routes from './routes/routes';
|
import routes from './routing/routes';
|
||||||
|
import history, { syncHistory } from './routing/history';
|
||||||
import 'file?name=index.html!../example/index.html';
|
import 'file?name=index.html!../example/index.html';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
|
|
||||||
|
// Create an enhanced history that syncs navigation events with the store
|
||||||
|
syncHistory(store);
|
||||||
|
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.id = 'root';
|
el.id = 'root';
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
|
|
||||||
render((
|
render((
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Routes/>
|
<Router history={history}>
|
||||||
|
{routes}
|
||||||
|
</Router>
|
||||||
</Provider>
|
</Provider>
|
||||||
), el);
|
), el);
|
||||||
|
@ -29,8 +29,8 @@ const PROVIDERS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class Authenticator {
|
class Authenticator {
|
||||||
constructor(config) {
|
constructor(config = {}) {
|
||||||
this.site_id = config.site_id;
|
this.site_id = config.site_id || null;
|
||||||
this.base_url = config.base_url || NETLIFY_API;
|
this.base_url = config.base_url || NETLIFY_API;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Map, List } from 'immutable';
|
import { Map, List } from 'immutable';
|
||||||
import { DRAFT_CREATE, DRAFT_DISCARD, DRAFT_CHANGE } from '../actions/entries';
|
import { DRAFT_CREATE_FROM_ENTRY, DRAFT_DISCARD, DRAFT_CHANGE } from '../actions/entries';
|
||||||
import { ADD_MEDIA, REMOVE_MEDIA } from '../actions/media';
|
import { ADD_MEDIA, REMOVE_MEDIA } from '../actions/media';
|
||||||
|
|
||||||
const initialState = Map({ entry: Map(), mediaFiles: List() });
|
const initialState = Map({ entry: Map(), mediaFiles: List() });
|
||||||
|
|
||||||
const entryDraft = (state = Map(), action) => {
|
const entryDraft = (state = Map(), action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case DRAFT_CREATE:
|
case DRAFT_CREATE_FROM_ENTRY:
|
||||||
if (!action.payload) {
|
if (!action.payload) {
|
||||||
// New entry
|
// New entry
|
||||||
return initialState;
|
return initialState;
|
||||||
@ -14,6 +14,7 @@ const entryDraft = (state = Map(), action) => {
|
|||||||
// Existing Entry
|
// Existing Entry
|
||||||
return state.withMutations((state) => {
|
return state.withMutations((state) => {
|
||||||
state.set('entry', action.payload);
|
state.set('entry', action.payload);
|
||||||
|
state.setIn(['entry', 'newRecord'], false);
|
||||||
state.set('mediaFiles', List());
|
state.set('mediaFiles', List());
|
||||||
});
|
});
|
||||||
case DRAFT_DISCARD:
|
case DRAFT_DISCARD:
|
||||||
@ -22,9 +23,9 @@ const entryDraft = (state = Map(), action) => {
|
|||||||
return state.set('entry', action.payload);
|
return state.set('entry', action.payload);
|
||||||
|
|
||||||
case ADD_MEDIA:
|
case ADD_MEDIA:
|
||||||
return state.update('mediaFiles', (list) => list.push(action.payload.uri));
|
return state.update('mediaFiles', (list) => list.push(action.payload.path));
|
||||||
case REMOVE_MEDIA:
|
case REMOVE_MEDIA:
|
||||||
return state.update('mediaFiles', (list) => list.filterNot((uri) => uri === action.payload));
|
return state.update('mediaFiles', (list) => list.filterNot((path) => path === action.payload));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
@ -23,5 +23,5 @@ export const selectEntry = (state, collection, slug) =>
|
|||||||
export const selectEntries = (state, collection) =>
|
export const selectEntries = (state, collection) =>
|
||||||
fromEntries.selectEntries(state.entries, collection);
|
fromEntries.selectEntries(state.entries, collection);
|
||||||
|
|
||||||
export const getMedia = (state, uri) =>
|
export const getMedia = (state, path) =>
|
||||||
fromMedias.getMedia(state.medias, uri);
|
fromMedias.getMedia(state.medias, path);
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
import { Map } from 'immutable';
|
import { Map } from 'immutable';
|
||||||
import { ADD_MEDIA, REMOVE_MEDIA } from '../actions/media';
|
import { ADD_MEDIA, REMOVE_MEDIA } from '../actions/media';
|
||||||
import { ENTRY_PERSIST_SUCCESS } from '../actions/entries';
|
|
||||||
import MediaProxy from '../valueObjects/MediaProxy';
|
import MediaProxy from '../valueObjects/MediaProxy';
|
||||||
|
|
||||||
const medias = (state = Map(), action) => {
|
const medias = (state = Map(), action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ADD_MEDIA:
|
case ADD_MEDIA:
|
||||||
return state.set(action.payload.uri, action.payload);
|
return state.set(action.payload.path, action.payload);
|
||||||
case REMOVE_MEDIA:
|
case REMOVE_MEDIA:
|
||||||
return state.delete(action.payload);
|
return state.delete(action.payload);
|
||||||
case ENTRY_PERSIST_SUCCESS:
|
|
||||||
return state.map((media, uri) => {
|
|
||||||
if (action.payload.persistedMediaFiles.indexOf(uri) > -1) media.uploaded = true;
|
|
||||||
return media;
|
|
||||||
});
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
@ -22,10 +16,10 @@ const medias = (state = Map(), action) => {
|
|||||||
|
|
||||||
export default medias;
|
export default medias;
|
||||||
|
|
||||||
export const getMedia = (state, uri) => {
|
export const getMedia = (state, path) => {
|
||||||
if (state.has(uri)) {
|
if (state.has(path)) {
|
||||||
return state.get(uri);
|
return state.get(path);
|
||||||
} else {
|
} else {
|
||||||
return new MediaProxy(uri, null, true);
|
return new MediaProxy(path, null, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
|
|
||||||
import App from '../containers/App';
|
|
||||||
import CollectionPage from '../containers/CollectionPage';
|
|
||||||
import EntryPage from '../containers/EntryPage';
|
|
||||||
import SearchPage from '../containers/SearchPage';
|
|
||||||
import NotFoundPage from '../containers/NotFoundPage';
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<Router history={browserHistory}>
|
|
||||||
<Route path="/" component={App}>
|
|
||||||
<IndexRoute component={CollectionPage}/>
|
|
||||||
<Route path="/collections/:name" component={CollectionPage}/>
|
|
||||||
<Route path="/collections/:name/entries/:slug" component={EntryPage}/>
|
|
||||||
<Route path="/search" component={SearchPage}/>
|
|
||||||
<Route path="*" component={NotFoundPage}/>
|
|
||||||
</Route>
|
|
||||||
</Router>
|
|
||||||
);
|
|
14
src/routing/history.js
Normal file
14
src/routing/history.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { createHashHistory } from 'history';
|
||||||
|
import { useRouterHistory } from 'react-router';
|
||||||
|
import { syncHistoryWithStore } from 'react-router-redux';
|
||||||
|
|
||||||
|
let history = useRouterHistory(createHashHistory)({
|
||||||
|
queryKey: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncHistory = (store) => {
|
||||||
|
history = syncHistoryWithStore(history, store);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { syncHistory };
|
||||||
|
export default history;
|
17
src/routing/routes.js
Normal file
17
src/routing/routes.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Route, IndexRoute } from 'react-router';
|
||||||
|
import App from '../containers/App';
|
||||||
|
import CollectionPage from '../containers/CollectionPage';
|
||||||
|
import EntryPage from '../containers/EntryPage';
|
||||||
|
import SearchPage from '../containers/SearchPage';
|
||||||
|
import NotFoundPage from '../containers/NotFoundPage';
|
||||||
|
|
||||||
|
export default (
|
||||||
|
<Route path="/" component={App}>
|
||||||
|
<IndexRoute component={CollectionPage}/>
|
||||||
|
<Route path="/collections/:name" component={CollectionPage}/>
|
||||||
|
<Route path="/collections/:name/entries/:slug" component={EntryPage}/>
|
||||||
|
<Route path="/search" component={SearchPage}/>
|
||||||
|
<Route path="*" component={NotFoundPage}/>
|
||||||
|
</Route>
|
||||||
|
);
|
@ -1,16 +1,15 @@
|
|||||||
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
|
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
|
||||||
import thunkMiddleware from 'redux-thunk';
|
import thunkMiddleware from 'redux-thunk';
|
||||||
import { browserHistory } from 'react-router';
|
import { routerReducer } from 'react-router-redux';
|
||||||
import { syncHistory, routeReducer } from 'react-router-redux';
|
|
||||||
import reducers from '../reducers';
|
import reducers from '../reducers';
|
||||||
|
|
||||||
const reducer = combineReducers({
|
const reducer = combineReducers({
|
||||||
...reducers,
|
...reducers,
|
||||||
router: routeReducer
|
routing: routerReducer
|
||||||
});
|
});
|
||||||
|
|
||||||
const createStoreWithMiddleware = compose(
|
const createStoreWithMiddleware = compose(
|
||||||
applyMiddleware(thunkMiddleware, syncHistory(browserHistory)),
|
applyMiddleware(thunkMiddleware),
|
||||||
window.devToolsExtension ? window.devToolsExtension() : (f) => f
|
window.devToolsExtension ? window.devToolsExtension() : (f) => f
|
||||||
)(createStore);
|
)(createStore);
|
||||||
|
|
||||||
|
@ -7,8 +7,22 @@ export default function MediaProxy(value, file, uploaded = false) {
|
|||||||
this.value = value;
|
this.value = value;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.uploaded = uploaded;
|
this.uploaded = uploaded;
|
||||||
this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + value : value;
|
this.sha = null;
|
||||||
this.toString = function() {
|
this.path = config.media_folder && !uploaded ? config.media_folder + '/' + value : value;
|
||||||
return this.uploaded ? this.uri : window.URL.createObjectURL(this.file, { oneTimeOnly: true });
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MediaProxy.prototype.toString = function() {
|
||||||
|
return this.uploaded ? this.path : window.URL.createObjectURL(this.file, { oneTimeOnly: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
MediaProxy.prototype.toBase64 = function() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fr = new FileReader();
|
||||||
|
fr.onload = (readerEvt) => {
|
||||||
|
const binaryString = readerEvt.target.result;
|
||||||
|
|
||||||
|
resolve(binaryString.split('base64,')[1]);
|
||||||
|
};
|
||||||
|
fr.readAsDataURL(this.file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/* global module, __dirname, require */
|
/* global module, __dirname, require */
|
||||||
var webpack = require('webpack');
|
var webpack = require('webpack');
|
||||||
|
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -12,7 +13,7 @@ module.exports = {
|
|||||||
{ test: /\.json$/, loader: 'json-loader' },
|
{ test: /\.json$/, loader: 'json-loader' },
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
loader: 'style!css?modules&importLoaders=1!postcss'
|
loader: ExtractTextPlugin.extract("style", "css?modules&importLoaders=1!postcss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loader: 'babel',
|
loader: 'babel',
|
||||||
@ -33,6 +34,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
|
new ExtractTextPlugin('cms.css', { allChunks: true }),
|
||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
|
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user