Merge branch 'contentIntegration' into react
This commit is contained in:
@ -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);
|
||||
});
|
||||
};
|
||||
|
Reference in New Issue
Block a user