Merge branch 'contentIntegration' into react
This commit is contained in:
commit
21c3d060ea
@ -17,6 +17,7 @@ collections: # A list of collections the CMS should be able to edit
|
||||
meta:
|
||||
- {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"}
|
||||
- {label: "SEO Description", name: "description", widget: "text"}
|
||||
card: {type: "image", image: "image", text: "title"}
|
||||
|
||||
- name: "faq" # Used in routes, ie.: /admin/collections/:slug/edit
|
||||
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
|
||||
- {label: "Question", name: "title", widget: "string", tagname: "h1"}
|
||||
- {label: "Answer", name: "body", widget: "markdown"}
|
||||
card: {type: "alltype", text: "title"}
|
||||
|
||||
- name: "settings"
|
||||
label: "Settings"
|
||||
|
@ -4,8 +4,9 @@
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<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>
|
||||
window.repoFiles = {
|
||||
_posts: {
|
||||
@ -16,6 +17,15 @@
|
||||
_faqs: {
|
||||
"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"
|
||||
},
|
||||
"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: {
|
||||
@ -30,7 +40,7 @@
|
||||
|
||||
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();
|
||||
|
||||
date.setTime(date.getTime() + ONE_DAY);
|
||||
@ -38,12 +48,25 @@
|
||||
var slug = dateString + "-post-number-" + i + ".md";
|
||||
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script src='/cms.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
|
10
package.json
10
package.json
@ -36,6 +36,7 @@
|
||||
"eslint-loader": "^1.2.1",
|
||||
"eslint-plugin-react": "^5.1.1",
|
||||
"exports-loader": "^0.6.3",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"express": "^4.13.4",
|
||||
"file-loader": "^0.8.5",
|
||||
"immutable": "^3.7.6",
|
||||
@ -53,8 +54,8 @@
|
||||
"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",
|
||||
"react-router": "^2.5.1",
|
||||
"react-router-redux": "^4.0.5",
|
||||
"redux": "^3.3.1",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"style-loader": "^0.13.0",
|
||||
@ -65,14 +66,17 @@
|
||||
"whatwg-fetch": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bricks.js": "^1.7.0",
|
||||
"commonmark": "^0.24.0",
|
||||
"commonmark-react-renderer": "^4.1.2",
|
||||
"draft-js": "^0.7.0",
|
||||
"draft-js-export-markdown": "^0.2.0",
|
||||
"draft-js-import-markdown": "^0.1.6",
|
||||
"fuzzy": "^0.1.1",
|
||||
"js-base64": "^2.1.9",
|
||||
"json-loader": "^0.5.4",
|
||||
"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) => {
|
||||
dispatch(configLoading());
|
||||
|
||||
fetch('/config.yml').then((response) => {
|
||||
fetch('config.yml').then((response) => {
|
||||
if (response.status !== 200) {
|
||||
throw `Failed to load config.yml (${response.status})`;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { currentBackend } from '../backends/backend';
|
||||
import { getMedia } from '../reducers';
|
||||
|
||||
/*
|
||||
* Contant Declarations
|
||||
@ -11,7 +12,7 @@ export const ENTRIES_REQUEST = 'ENTRIES_REQUEST';
|
||||
export const ENTRIES_SUCCESS = 'ENTRIES_SUCCESS';
|
||||
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_CHANGE = 'DRAFT_CHANGE';
|
||||
|
||||
@ -69,7 +70,7 @@ function entriesFailed(collection, error) {
|
||||
type: ENTRIES_FAILURE,
|
||||
error: 'Failed to load entries',
|
||||
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 {
|
||||
type: ENTRY_PERSIST_SUCCESS,
|
||||
payload: {
|
||||
persistedEntry: persistedEntry,
|
||||
persistedMediaFiles: persistedMediaFiles
|
||||
collection: collection,
|
||||
entry: entry
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -104,9 +105,9 @@ function entryPersistFail(collection, entry, error) {
|
||||
/*
|
||||
* Exported simple Action Creators
|
||||
*/
|
||||
export function createDraft(entry) {
|
||||
export function createDraftFromEntry(entry) {
|
||||
return {
|
||||
type: DRAFT_CREATE,
|
||||
type: DRAFT_CREATE_FROM_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) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const MediaProxies = entry.get('mediaFiles').map(path => getMedia(state, path));
|
||||
|
||||
dispatch(entryPersisting(collection, entry));
|
||||
backend.persist(collection, entry, mediaFiles).then(
|
||||
({persistedEntry, persistedMediaFiles}) => {
|
||||
dispatch(entryPersisted(persistedEntry, persistedMediaFiles));
|
||||
backend.persistEntry(collection, entry, MediaProxies.toJS()).then(
|
||||
() => {
|
||||
dispatch(entryPersisted(collection, entry));
|
||||
},
|
||||
(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';
|
||||
|
||||
export const RUN_COMMAND = 'RUN_COMMAND';
|
||||
export const LIST_POSTS = 'LIST_POSTS';
|
||||
export const LIST_FAQ = 'LIST_FAQ';
|
||||
export const SHOW_COLLECTION = 'SHOW_COLLECTION';
|
||||
export const CREATE_COLLECTION = 'CREATE_COLLECTION';
|
||||
export const HELP = 'HELP';
|
||||
|
||||
export function run(commandName, payload) {
|
||||
return { type: RUN_COMMAND, command: commandName, payload };
|
||||
}
|
||||
|
||||
|
||||
export function runCommand(commandName, payload) {
|
||||
return (dispatch, getState) => {
|
||||
switch (commandName) {
|
||||
case LIST_POSTS:
|
||||
browserHistory.push('/collections/posts');
|
||||
case SHOW_COLLECTION:
|
||||
history.push(`/collections/${payload.collectionName}`);
|
||||
break;
|
||||
case LIST_FAQ:
|
||||
browserHistory.push('/collections/faq');
|
||||
case CREATE_COLLECTION:
|
||||
window.alert(`Create a new ${payload.collectionName} - not supported yet`);
|
||||
break;
|
||||
case HELP:
|
||||
window.alert('Find Bar Help (PLACEHOLDER)\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
||||
break;
|
||||
case SEARCH:
|
||||
browserHistory.push('/search');
|
||||
history.push('/search');
|
||||
break;
|
||||
}
|
||||
dispatch(run(commandName, payload));
|
||||
|
@ -2,9 +2,9 @@ export const ADD_MEDIA = 'ADD_MEDIA';
|
||||
export const REMOVE_MEDIA = 'REMOVE_MEDIA';
|
||||
|
||||
export function addMedia(mediaProxy) {
|
||||
return {type: ADD_MEDIA, payload: mediaProxy};
|
||||
return { type: ADD_MEDIA, payload: mediaProxy };
|
||||
}
|
||||
|
||||
export function removeMedia(uri) {
|
||||
return {type: REMOVE_MEDIA, payload: uri};
|
||||
export function removeMedia(path) {
|
||||
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 entryObj = {
|
||||
path: entryDraft.getIn(['entry', 'path']),
|
||||
slug: entryDraft.getIn(['entry', 'slug']),
|
||||
raw: this.entryToRaw(collection, entryData)
|
||||
};
|
||||
return this.implementation.persist(collection, entryObj, entryDraft.get('mediaFiles').toJS()).then(
|
||||
(response) => ({
|
||||
persistedEntry: this.entryWithFormat(collection)(response.persistedEntry),
|
||||
persistedMediaFiles:response.persistedMediaFiles
|
||||
})
|
||||
);
|
||||
|
||||
const commitMessage = (entryDraft.getIn(['entry', 'newRecord']) ? 'Created ' : 'Updated ') +
|
||||
collection.get('label') + ' “' +
|
||||
entryDraft.getIn(['entry', 'data', 'title']) + '”';
|
||||
|
||||
return this.implementation.persistEntry(collection, entryObj, MediaFiles, { commitMessage });
|
||||
}
|
||||
|
||||
entryToRaw(collection, entry) {
|
||||
|
@ -14,9 +14,14 @@ export default class AuthenticationPage extends React.Component {
|
||||
|
||||
handleLogin(e) {
|
||||
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: 'user'}, (err, data) => {
|
||||
auth.authenticate({provider: 'github', scope: 'repo'}, (err, data) => {
|
||||
if (err) {
|
||||
this.setState({loginError: err.toString()});
|
||||
return;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import LocalForage from 'localforage';
|
||||
import MediaProxy from '../../valueObjects/MediaProxy';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
const API_ROOT = 'https://api.github.com';
|
||||
|
||||
@ -8,7 +10,7 @@ class API {
|
||||
this.token = token;
|
||||
this.repo = repo;
|
||||
this.branch = branch;
|
||||
this.baseURL = API_ROOT + `/repos/${this.repo}`;
|
||||
this.repoURL = `/repos/${this.repo}`;
|
||||
}
|
||||
|
||||
user() {
|
||||
@ -20,9 +22,9 @@ class API {
|
||||
return cache.then((cached) => {
|
||||
if (cached) { return cached; }
|
||||
|
||||
return this.request(`/contents/${path}`, {
|
||||
headers: {Accept: 'application/vnd.github.VERSION.raw'},
|
||||
data: {ref: this.branch},
|
||||
return this.request(`${this.repoURL}/contents/${path}`, {
|
||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||
body: { ref: this.branch },
|
||||
cache: false
|
||||
}).then((result) => {
|
||||
if (sha) {
|
||||
@ -35,11 +37,48 @@ class API {
|
||||
}
|
||||
|
||||
listFiles(path) {
|
||||
return this.request(`/contents/${path}`, {
|
||||
data: {ref: this.branch}
|
||||
return this.request(`${this.repoURL}/contents/${path}`, {
|
||||
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 = {}) {
|
||||
return {
|
||||
Authorization: `token ${this.token}`,
|
||||
@ -60,7 +99,7 @@ class API {
|
||||
|
||||
request(path, options = {}) {
|
||||
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/)) {
|
||||
return this.parseJsonResponse(response);
|
||||
}
|
||||
@ -68,6 +107,78 @@ class API {
|
||||
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 {
|
||||
@ -115,4 +226,8 @@ export default class GitHub {
|
||||
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) {
|
||||
return Promise.resolve({email: state.email});
|
||||
return Promise.resolve({ email: state.email });
|
||||
}
|
||||
|
||||
entries(collection) {
|
||||
@ -48,10 +48,11 @@ export default class TestRepo {
|
||||
));
|
||||
}
|
||||
|
||||
persist(collection, entry, mediaFiles = []) {
|
||||
const folder = collection.get('folder');
|
||||
persistEntry(collection, entry, mediaFiles = []) {
|
||||
const folder = entry.path.substring(0, entry.path.lastIndexOf('/'));
|
||||
const fileName = entry.path.substring(entry.path.lastIndexOf('/') + 1);
|
||||
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 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 }) {
|
||||
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>;
|
||||
export default class EntryListing extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.bricksInstance = null;
|
||||
|
||||
this.bricksConfig = {
|
||||
packed: 'data-packed',
|
||||
sizes: [
|
||||
{ columns: 1, gutter: 15 },
|
||||
{ mq: '495px', columns: 2, gutter: 15 },
|
||||
{ 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 = {
|
||||
|
@ -4,10 +4,12 @@
|
||||
composes: rounded from "../theme.css";
|
||||
composes: depth from "../theme.css";
|
||||
overflow: hidden;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import styles from './Card.css';
|
||||
|
||||
export default function Card({ style, className = '', children }) {
|
||||
return <div className={`${styles.card} ${className}`} style={style}>{children}</div>;
|
||||
export default function Card({ style, className = '', onClick, children }) {
|
||||
return <div className={`${styles.card} ${className}`} style={style} onClick={onClick}>{children}</div>;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
:root {
|
||||
--defaultColor: #333;
|
||||
--backgroundColor: #fff;
|
||||
--shadowColor: rgba(0, 0, 0, 0.25);
|
||||
--shadowColor: rgba(0, 0, 0, 0.117647);
|
||||
--successColor: #1c7;
|
||||
--warningColor: #fa0;
|
||||
--errorColor: #f52;
|
||||
@ -12,15 +12,14 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 10px;
|
||||
color: var(--defaultColor);
|
||||
background-color: var(--backgroundColor);
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.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 UnknownPreview from './widgets/UnknownPreview';
|
||||
import StringControl from './widgets/StringControl';
|
||||
import StringPreview from './widgets/StringPreview';
|
||||
import MarkdownControl from './widgets/MarkdownControl';
|
||||
import MarkdownPreview from './widgets/MarkdownPreview';
|
||||
import ImageControl from './widgets/ImageControl';
|
||||
import ImagePreview from './widgets/ImagePreview';
|
||||
import UnknownControl from './Widgets/UnknownControl';
|
||||
import UnknownPreview from './Widgets/UnknownPreview';
|
||||
import StringControl from './Widgets/StringControl';
|
||||
import StringPreview from './Widgets/StringPreview';
|
||||
import MarkdownControl from './Widgets/MarkdownControl';
|
||||
import MarkdownPreview from './Widgets/MarkdownPreview';
|
||||
import ImageControl from './Widgets/ImageControl';
|
||||
import ImagePreview from './Widgets/ImagePreview';
|
||||
|
||||
|
||||
const Widgets = {
|
||||
|
@ -53,7 +53,7 @@ export default class ImageControl extends React.Component {
|
||||
if (file) {
|
||||
const mediaProxy = new MediaProxy(file.name, file);
|
||||
this.props.onAddMedia(mediaProxy);
|
||||
this.props.onChange(mediaProxy.uri);
|
||||
this.props.onChange(mediaProxy.path);
|
||||
} else {
|
||||
this.props.onChange(null);
|
||||
}
|
||||
@ -63,7 +63,7 @@ export default class ImageControl extends React.Component {
|
||||
renderImageName() {
|
||||
if (!this.props.value) return null;
|
||||
if (this.value instanceof MediaProxy) {
|
||||
return truncateMiddle(this.props.value.uri, MAX_DISPLAY_LENGTH);
|
||||
return truncateMiddle(this.props.value.path, MAX_DISPLAY_LENGTH);
|
||||
} else {
|
||||
return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH);
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { Card } from '../UI';
|
||||
import { storiesOf } from '@kadira/storybook';
|
||||
|
||||
const styles = {
|
||||
fixedWidth: { width: 280 },
|
||||
footer: {
|
||||
color: '#aaa',
|
||||
backgroundColor: '#555',
|
||||
@ -11,40 +10,33 @@ const styles = {
|
||||
marginTop: 5,
|
||||
padding: 10
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
storiesOf('Card', module)
|
||||
.add('Default View', () => (
|
||||
<div style={styles.fixedWidth}>
|
||||
<Card>
|
||||
<h1>A Card</h1>
|
||||
<h2>Subtitle</h2>
|
||||
<p>
|
||||
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
|
||||
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.
|
||||
In consequat egestas nulla condimentum faucibus. Donec scelerisque convallis est nec fringila. Suspendisse
|
||||
non lorem non erat congue consequat.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
<Card>
|
||||
<h1>A Card</h1>
|
||||
<h2>Subtitle</h2>
|
||||
<p>
|
||||
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
|
||||
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.
|
||||
In consequat egestas nulla condimentum faucibus. Donec scelerisque convallis est nec fringila. Suspendisse
|
||||
non lorem non erat congue consequat.
|
||||
</p>
|
||||
</Card>
|
||||
)).add('Full width content', () => (
|
||||
<div style={styles.fixedWidth}>
|
||||
<Card>
|
||||
<img src="https://i.ytimg.com/vi/tntOCGkgt98/maxresdefault.jpg" />
|
||||
<h1>Card & cat</h1>
|
||||
<p>Media Elements such as video, img (and iFrame for embeds) don't have margin</p>
|
||||
</Card>
|
||||
</div>
|
||||
<Card>
|
||||
<img src="https://i.ytimg.com/vi/tntOCGkgt98/maxresdefault.jpg" />
|
||||
<h1>Card & cat</h1>
|
||||
<p>Media Elements such as video, img (and iFrame for embeds) don't have margin</p>
|
||||
</Card>
|
||||
)).add('Footer', () => (
|
||||
<div style={styles.fixedWidth}>
|
||||
<Card>
|
||||
<img src="http://www.top13.net/wp-content/uploads/2015/10/perfectly-timed-funny-cat-pictures-5.jpg" />
|
||||
<h1>Now with footer.</h1>
|
||||
<p>header and footer elements are also not subject to margin</p>
|
||||
<footer style={styles.footer}>© Thousand Cats Corp</footer>
|
||||
</Card>
|
||||
</div>
|
||||
<Card>
|
||||
<img src="http://www.top13.net/wp-content/uploads/2015/10/perfectly-timed-funny-cat-pictures-5.jpg" />
|
||||
<h1>Now with footer.</h1>
|
||||
<p>header and footer elements are also not subject to margin</p>
|
||||
<footer style={styles.footer}>© Thousand Cats Corp</footer>
|
||||
</Card>
|
||||
))
|
||||
|
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 { loginUser } from '../actions/auth';
|
||||
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 styles from './App.css';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
class App extends React.Component {
|
||||
componentDidMount() {
|
||||
@ -49,6 +51,37 @@ class App extends React.Component {
|
||||
</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() {
|
||||
const { user, config, children } = this.props;
|
||||
|
||||
@ -68,24 +101,31 @@ class App extends React.Component {
|
||||
return this.authenticating();
|
||||
}
|
||||
|
||||
const { commands, defaultCommands } = this.generateFindBarCommands();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FindBar commands={[
|
||||
{ id: LIST_POSTS, pattern: 'List Posts' },
|
||||
{ id: LIST_FAQ, pattern: 'List FAQs' },
|
||||
{ id: HELP, pattern: 'Help' },
|
||||
]} />
|
||||
{children}
|
||||
<header>
|
||||
<div className={styles.alignable}>
|
||||
<FindBar
|
||||
commands={commands}
|
||||
defaultCommands={defaultCommands}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div className={`${styles.alignable} ${styles.main}`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const { auth, config } = state;
|
||||
const { auth, config, collections } = state;
|
||||
const user = auth && auth.get('user');
|
||||
|
||||
return { auth, config, user };
|
||||
return { auth, config, collections, user };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(App);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Link } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import { loadEntries } from '../actions/entries';
|
||||
import { selectEntries } from '../reducers';
|
||||
@ -29,17 +28,7 @@ class DashboardPage extends React.Component {
|
||||
}
|
||||
|
||||
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>
|
||||
{entries ? <EntryListing collection={collection} entries={entries}/> : 'Loading entries...'}
|
||||
</div>
|
||||
{entries ? <EntryListing collection={collection} entries={entries}/> : 'Loading entries...'}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
loadEntry,
|
||||
createDraft,
|
||||
createDraftFromEntry,
|
||||
discardDraft,
|
||||
changeDraft,
|
||||
persist
|
||||
persistEntry
|
||||
} from '../actions/entries';
|
||||
import { addMedia, removeMedia } from '../actions/media';
|
||||
import { selectEntry, getMedia } from '../reducers';
|
||||
@ -16,18 +16,18 @@ class EntryPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.props.loadEntry(props.collection, props.slug);
|
||||
this.handlePersist = this.handlePersist.bind(this);
|
||||
this.handlePersistEntry = this.handlePersistEntry.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.entry) {
|
||||
this.props.createDraft(this.props.entry);
|
||||
this.props.createDraftFromEntry(this.props.entry);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
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();
|
||||
}
|
||||
|
||||
handlePersist() {
|
||||
this.props.persist(this.props.collection, this.props.entryDraft);
|
||||
handlePersistEntry() {
|
||||
this.props.persistEntry(this.props.collection, this.props.entryDraft);
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -56,7 +56,7 @@ class EntryPage extends React.Component {
|
||||
onChange={changeDraft}
|
||||
onAddMedia={addMedia}
|
||||
onRemoveMedia={removeMedia}
|
||||
onPersist={this.handlePersist}
|
||||
onPersist={this.handlePersistEntry}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -67,12 +67,12 @@ EntryPage.propTypes = {
|
||||
boundGetMedia: PropTypes.func.isRequired,
|
||||
changeDraft: PropTypes.func.isRequired,
|
||||
collection: ImmutablePropTypes.map.isRequired,
|
||||
createDraft: PropTypes.func.isRequired,
|
||||
createDraftFromEntry: PropTypes.func.isRequired,
|
||||
discardDraft: PropTypes.func.isRequired,
|
||||
entry: ImmutablePropTypes.map.isRequired,
|
||||
entryDraft: ImmutablePropTypes.map.isRequired,
|
||||
loadEntry: PropTypes.func.isRequired,
|
||||
persist: PropTypes.func.isRequired,
|
||||
persistEntry: PropTypes.func.isRequired,
|
||||
removeMedia: PropTypes.func.isRequired,
|
||||
slug: PropTypes.string.isRequired,
|
||||
};
|
||||
@ -93,8 +93,8 @@ export default connect(
|
||||
addMedia,
|
||||
removeMedia,
|
||||
loadEntry,
|
||||
createDraft,
|
||||
createDraftFromEntry,
|
||||
discardDraft,
|
||||
persist
|
||||
persistEntry
|
||||
}
|
||||
)(EntryPage);
|
||||
|
@ -10,8 +10,8 @@
|
||||
position: relative;
|
||||
background-color: var(--backgroundColor);
|
||||
padding: 1px 0;
|
||||
margin: 4px auto;
|
||||
}
|
||||
|
||||
.inputArea {
|
||||
display: table;
|
||||
width: calc(100% - 10px);
|
||||
|
@ -124,8 +124,11 @@ class FindBar extends Component {
|
||||
}, () => {
|
||||
this._input.blur();
|
||||
});
|
||||
const payload = paramName ? { [paramName]: enteredParamValue } : null;
|
||||
this.props.dispatch(runCommand(command.id, payload));
|
||||
const payload = command.payload || {};
|
||||
if (paramName) {
|
||||
payload[paramName] = enteredParamValue;
|
||||
}
|
||||
this.props.dispatch(runCommand(command.type, payload));
|
||||
}
|
||||
}
|
||||
|
||||
@ -358,6 +361,7 @@ class FindBar extends Component {
|
||||
FindBar.propTypes = {
|
||||
commands: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
pattern: PropTypes.string.isRequired
|
||||
})).isRequired,
|
||||
defaultCommands: PropTypes.arrayOf(PropTypes.string),
|
||||
|
@ -19,7 +19,7 @@ const ImageType = new yaml.Type('image', {
|
||||
kind: 'scalar',
|
||||
instanceOf: MediaProxy,
|
||||
represent: function(value) {
|
||||
return `${value.uri}`;
|
||||
return `${value.path}`;
|
||||
},
|
||||
resolve: function(value) {
|
||||
if (value === null) return false;
|
||||
|
@ -2,6 +2,7 @@ html {
|
||||
box-sizing: border-box;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
@ -10,13 +11,23 @@ html {
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
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 > * {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, figure, dl, ol, ul {
|
||||
h1, h2, h3, h4, h5, h6, p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
11
src/index.js
11
src/index.js
@ -1,19 +1,26 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router';
|
||||
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 './index.css';
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
// Create an enhanced history that syncs navigation events with the store
|
||||
syncHistory(store);
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.id = 'root';
|
||||
document.body.appendChild(el);
|
||||
|
||||
render((
|
||||
<Provider store={store}>
|
||||
<Routes/>
|
||||
<Router history={history}>
|
||||
{routes}
|
||||
</Router>
|
||||
</Provider>
|
||||
), el);
|
||||
|
@ -29,8 +29,8 @@ const PROVIDERS = {
|
||||
};
|
||||
|
||||
class Authenticator {
|
||||
constructor(config) {
|
||||
this.site_id = config.site_id;
|
||||
constructor(config = {}) {
|
||||
this.site_id = config.site_id || null;
|
||||
this.base_url = config.base_url || NETLIFY_API;
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
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';
|
||||
|
||||
const initialState = Map({ entry: Map(), mediaFiles: List() });
|
||||
|
||||
const entryDraft = (state = Map(), action) => {
|
||||
switch (action.type) {
|
||||
case DRAFT_CREATE:
|
||||
case DRAFT_CREATE_FROM_ENTRY:
|
||||
if (!action.payload) {
|
||||
// New entry
|
||||
return initialState;
|
||||
@ -14,6 +14,7 @@ const entryDraft = (state = Map(), action) => {
|
||||
// Existing Entry
|
||||
return state.withMutations((state) => {
|
||||
state.set('entry', action.payload);
|
||||
state.setIn(['entry', 'newRecord'], false);
|
||||
state.set('mediaFiles', List());
|
||||
});
|
||||
case DRAFT_DISCARD:
|
||||
@ -22,9 +23,9 @@ const entryDraft = (state = Map(), action) => {
|
||||
return state.set('entry', action.payload);
|
||||
|
||||
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:
|
||||
return state.update('mediaFiles', (list) => list.filterNot((uri) => uri === action.payload));
|
||||
return state.update('mediaFiles', (list) => list.filterNot((path) => path === action.payload));
|
||||
|
||||
default:
|
||||
return state;
|
||||
|
@ -23,5 +23,5 @@ export const selectEntry = (state, collection, slug) =>
|
||||
export const selectEntries = (state, collection) =>
|
||||
fromEntries.selectEntries(state.entries, collection);
|
||||
|
||||
export const getMedia = (state, uri) =>
|
||||
fromMedias.getMedia(state.medias, uri);
|
||||
export const getMedia = (state, path) =>
|
||||
fromMedias.getMedia(state.medias, path);
|
||||
|
@ -1,19 +1,13 @@
|
||||
import { Map } from 'immutable';
|
||||
import { ADD_MEDIA, REMOVE_MEDIA } from '../actions/media';
|
||||
import { ENTRY_PERSIST_SUCCESS } from '../actions/entries';
|
||||
import MediaProxy from '../valueObjects/MediaProxy';
|
||||
|
||||
const medias = (state = Map(), action) => {
|
||||
switch (action.type) {
|
||||
case ADD_MEDIA:
|
||||
return state.set(action.payload.uri, action.payload);
|
||||
return state.set(action.payload.path, action.payload);
|
||||
case REMOVE_MEDIA:
|
||||
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:
|
||||
return state;
|
||||
@ -22,10 +16,10 @@ const medias = (state = Map(), action) => {
|
||||
|
||||
export default medias;
|
||||
|
||||
export const getMedia = (state, uri) => {
|
||||
if (state.has(uri)) {
|
||||
return state.get(uri);
|
||||
export const getMedia = (state, path) => {
|
||||
if (state.has(path)) {
|
||||
return state.get(path);
|
||||
} 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 thunkMiddleware from 'redux-thunk';
|
||||
import { browserHistory } from 'react-router';
|
||||
import { syncHistory, routeReducer } from 'react-router-redux';
|
||||
import { routerReducer } from 'react-router-redux';
|
||||
import reducers from '../reducers';
|
||||
|
||||
const reducer = combineReducers({
|
||||
...reducers,
|
||||
router: routeReducer
|
||||
routing: routerReducer
|
||||
});
|
||||
|
||||
const createStoreWithMiddleware = compose(
|
||||
applyMiddleware(thunkMiddleware, syncHistory(browserHistory)),
|
||||
applyMiddleware(thunkMiddleware),
|
||||
window.devToolsExtension ? window.devToolsExtension() : (f) => f
|
||||
)(createStore);
|
||||
|
||||
|
@ -7,8 +7,22 @@ export default function MediaProxy(value, file, uploaded = false) {
|
||||
this.value = value;
|
||||
this.file = file;
|
||||
this.uploaded = uploaded;
|
||||
this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + value : value;
|
||||
this.toString = function() {
|
||||
return this.uploaded ? this.uri : window.URL.createObjectURL(this.file, { oneTimeOnly: true });
|
||||
};
|
||||
this.sha = null;
|
||||
this.path = config.media_folder && !uploaded ? config.media_folder + '/' + value : value;
|
||||
}
|
||||
|
||||
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 */
|
||||
var webpack = require('webpack');
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
var path = require('path');
|
||||
|
||||
module.exports = {
|
||||
@ -12,7 +13,7 @@ module.exports = {
|
||||
{ test: /\.json$/, loader: 'json-loader' },
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: 'style!css?modules&importLoaders=1!postcss'
|
||||
loader: ExtractTextPlugin.extract("style", "css?modules&importLoaders=1!postcss"),
|
||||
},
|
||||
{
|
||||
loader: 'babel',
|
||||
@ -33,6 +34,7 @@ module.exports = {
|
||||
],
|
||||
|
||||
plugins: [
|
||||
new ExtractTextPlugin('cms.css', { allChunks: true }),
|
||||
new webpack.ProvidePlugin({
|
||||
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user