This commit is contained in:
Jori Lallo
2016-04-28 22:25:37 -07:00
parent 2f9233222d
commit cce82b3d43
79 changed files with 1495 additions and 496 deletions

View File

@@ -9,6 +9,10 @@ export const TOGGLE_EDITORS = 'TOGGLE_EDITORS';
export const ADD_REVISION = 'ADD_REVISION';
export const REPLACE_TEXT= 'REPLACE_TEXT';
export const SLACK_AUTH_PENDING = 'SLACK_AUTH_PENDING';
export const SLACK_AUTH_SUCCESS = 'SLACK_AUTH_SUCCESS';
export const SLACK_AUTH_FAILURE = 'SLACK_AUTH_FAILURE';
/*
* Other Constants
*/

View File

@@ -1,13 +1,10 @@
import React from 'react';
import MarkdownIcon from '../../Components/Icons/Markdown';
import styles from './Header.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const Header = ({
activeEditors,
toggleEditors,
}) => {
return (
@@ -17,16 +14,7 @@ const Header = ({
className={ cx('headerItem', 'editorToggle') }
style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}
>
<div
onClick={toggleEditors.bind(this, 'MARKDOWN')}
className={ activeEditors.includes('MARKDOWN') ? styles.active : '' }
>
<MarkdownIcon style={{ width: '32px', height: '20px', color: '#fff' }} />
</div>
<div
onClick={toggleEditors.bind(this, 'TEXT')}
className={ activeEditors.includes('TEXT') ? styles.active : '' }
>
<div>
<span className={ styles.textIcon }>Aa</span>
</div>
</div>

View File

@@ -19,7 +19,7 @@
width: 150px;
padding: 12px 22px;
font-size: 13px;
font-size: 15px;
font-weight: 300;
font-family: "Atlas Grotesk", "Helvetica Neue", sans-serif;
text-align: center;

View File

@@ -3,12 +3,13 @@ import Codemirror from 'react-codemirror';
import 'codemirror/mode/gfm/gfm';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/addon/edit/continuelist';
import 'codemirror/addon/display/placeholder.js';
import Dropzone from 'react-dropzone';
import styles from './MarkdownEditor.scss';
import './codemirror.css';
import './codemirror.scss';
import { client } from '../../Utils/ApiClient';
import { client } from '../../utils/ApiClient';
class MarkdownAtlas extends React.Component {
static propTypes = {
@@ -97,6 +98,7 @@ class MarkdownAtlas extends React.Component {
extraKeys: {
Enter: 'newlineAndIndentContinueMarkdownList',
},
placeholder: "# Start with a title...",
};
// http://codepen.io/lubelski/pen/fnGae

View File

@@ -1,65 +0,0 @@
/*
Name: Base16 Default Light
Author: Chris Kempson (http://chriskempson.com)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
@import url(https://fonts.googleapis.com/css?family=Cousine:400,700,700italic,400italic);
/* Custom styling */
.cm-s-atlas.CodeMirror {
background: #ffffff;
color: #202020;
font-family: 'Atlas Typewriter', 'Cousine', 'Monaco', monospace;
font-weight: 300;
height: auto;
}
.cm-s-atlas div.CodeMirror-selected {
background: #90CAF9;
}
/* Disable ondrag cursor for file uploads */
.cm-s-atlas div.CodeMirror-dragcursors {
visibility: hidden;
}
.cm-s-atlas .CodeMirror-line::selection,
.cm-s-atlas .CodeMirror-line > span::selection,
.cm-s-atlas .CodeMirror-line > span > span::selection {
background: #90CAF9;
}
.cm-s-atlas .CodeMirror-line::-moz-selection, .cm-s-atlas .CodeMirror-line > span::-moz-selection, .cm-s-atlas .CodeMirror-line > span > span::-moz-selection { background: #e0e0e0; }
.cm-s-atlas .CodeMirror-gutters { background: #f5f5f5; border-right: 0px; }
.cm-s-atlas .CodeMirror-guttermarker { color: #ac4142; }
.cm-s-atlas .CodeMirror-guttermarker-subtle { color: #b0b0b0; }
.cm-s-atlas .CodeMirror-linenumber { color: #b0b0b0; }
.cm-s-atlas .CodeMirror-cursor {
border-left: 2px solid #2196F3;
}
.cm-s-atlas span.cm-quote {
font-style: italic;
}
.cm-s-atlas span.cm-comment { color: #8f5536; }
.cm-s-atlas span.cm-atom { color: #aa759f; }
.cm-s-atlas span.cm-number { color: #aa759f; }
.cm-s-atlas span.cm-property, .cm-s-atlas span.cm-attribute { color: #90a959; }
.cm-s-atlas span.cm-keyword { color: #ac4142; }
.cm-s-atlas span.cm-string { color: #f4bf75; }
.cm-s-atlas span.cm-variable { color: #90a959; }
.cm-s-atlas span.cm-variable-2 { color: #788696; }
.cm-s-atlas span.cm-def { color: #d28445; }
.cm-s-atlas span.cm-bracket { color: #202020; }
.cm-s-atlas span.cm-tag { color: #ac4142; }
.cm-s-atlas span.cm-link { color: #aa759f; }
.cm-s-atlas span.cm-error { background: #ac4142; color: #505050; }
.cm-s-atlas .CodeMirror-activeline-background { background: #DDDCDC; }
.cm-s-atlas .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }

View File

@@ -11,9 +11,7 @@ const keys = keyMirror({
// Constant values
const constants = {
API_USER_AGENT: `${name}/${version}`,
API_BASE_URL: 'http://localhost:8000/api',
LOGIN_PATH: '/login',
LOGIN_SUCCESS_PATH: '/dashboard',
API_BASE_URL: 'http://localhost:3000/api',
};
export default Object.assign(keys, constants);

View File

@@ -7,8 +7,8 @@ import {
TOGGLE_HISTORY_SIDEBAR,
ADD_REVISION,
REPLACE_TEXT,
ActiveEditors,
} from '../Actions';
ActiveEditors
} from '../actions';
const activeEditors = (state = [ActiveEditors.MARKDOWN, ActiveEditors.TEXT], action) => {
switch (action.type) {
@@ -103,8 +103,13 @@ const text = (state = textDefaultState, action) => {
}
};
import team from './team';
import user from './user';
export default combineReducers({
activeEditors,
historySidebar,
text,
user,
team,
});

View File

@@ -1,12 +1,12 @@
import _ from 'lodash';
import Auth from './Auth';
import Constants from '../Constants';
import auth from './auth';
import constants from '../constants';
class ApiClient {
constructor(options = {}) {
this.baseUrl = options.baseUrl || Constants.API_BASE_URL;
this.userAgent = options.userAgent || Constants.API_USER_AGENT;
this.baseUrl = options.baseUrl || constants.API_BASE_URL;
this.userAgent = options.userAgent || constants.API_USER_AGENT;
}
fetch = (path, method, data) => {
@@ -25,8 +25,8 @@ class ApiClient {
'Content-Type': 'application/json',
'User-Agent': this.userAgent,
});
if (Auth.getToken()) {
headers.set('Authorization', `JWT ${Auth.getToken()}`);
if (auth.getToken()) {
headers.set('Authorization', `Bearer ${auth.getToken()}`);
}
// Construct request
@@ -48,7 +48,7 @@ class ApiClient {
// Handle 401, log out user
if (response.status === 401) {
Auth.logout();
auth.logout(); // replace with dispatch+action
}
// Handle failed responses
@@ -81,18 +81,6 @@ class ApiClient {
return this.fetch(path, 'POST', data);
}
put = (path, data) => {
return this.fetch(path, 'PUT', data);
}
get = (path, data) => {
return this.fetch(path, 'GET', data);
}
delete = (path, data) => {
return this.fetch(path, 'DELETE', data);
}
// Helpers
constructQueryString = (data) => {

View File

@@ -1,42 +1,19 @@
// Inspired by https://github.com/reactjs/react-router/blob/master/examples/auth-flow/auth.js
import Constants from '../Constants';
import History from './History';
import { client } from './ApiClient';
import constants from '../constants';
export default {
login(email, password) {
return new Promise((resolve, reject) => {
client.post('/authenticate', {
email,
password,
})
.then((data) => {
localStorage.setItem(Constants.JWT_STORE_KEY, data.jwt_token);
this.onChange(true);
resolve(data);
})
.catch((error) => {
reject(error);
});
});
setToken(token) {
localStorage.setItem(constants.JWT_STORE_KEY, token);
},
getToken() {
return localStorage.getItem(Constants.JWT_STORE_KEY);
return localStorage.getItem(constants.JWT_STORE_KEY);
},
logout() {
localStorage.removeItem(Constants.JWT_STORE_KEY);
History.push(Constants.LOGIN_PATH);
this.onChange(false);
localStorage.removeItem(constants.JWT_STORE_KEY);
},
loggedIn() {
return !!localStorage.getItem(Constants.JWT_STORE_KEY);
},
onChange() {
// This is overriden with a callback function in `Views/App/App.js`
return !!localStorage.getItem(constants.JWT_STORE_KEY);
},
};

View File

@@ -1,89 +0,0 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import MarkdownEditor from '../../Components/MarkdownEditor';
import TextEditor from '../../Components/TextEditor';
import { toMarkdown } from '../../Utils/Markdown';
import {
updateText,
replaceText,
} from '../../Actions';
import Constants from '../../Constants';
import styles from './Dashboard.scss';
class Dashboard extends Component {
static propTypes = {
editMarkdown: React.PropTypes.func.isRequired,
editText: React.PropTypes.func.isRequired,
text: React.PropTypes.string,
replaceText: React.PropTypes.func.isRequired,
activeEditors: React.PropTypes.arrayOf(React.PropTypes.string),
showHistorySidebar: React.PropTypes.bool.isRequired,
}
render() {
const activeEditors = this.props.activeEditors;
return (
<div className={styles.container}>
{
activeEditors.includes('MARKDOWN') ? (
<div className={ `${activeEditors.length > 1 ?
styles.panel : styles.fullscreen} ${styles.markdown}`}
>
<MarkdownEditor
onChange={this.props.editMarkdown}
text={this.props.text}
replaceText={this.props.replaceText}
/>
</div>
) : null
}
{
activeEditors.includes('TEXT') ? (
<div className={ `${activeEditors.length > 1 ?
styles.panel : styles.fullscreen} ${styles.text}`}
>
<TextEditor onChange={this.props.editText} text={this.props.text} />
</div>
) : null
}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
text: state.text.text,
editor: state.editor,
activeEditors: state.activeEditors,
showHistorySidebar: state.historySidebar.visible,
revisions: state.text.revisions,
};
};
const mapDispatchToProps = (dispatch) => {
return {
editMarkdown: (text) => {
dispatch(updateText(text, 'markdown'));
},
editText: (html) => {
const text = toMarkdown(html);
dispatch(updateText(text, 'text'));
},
replaceText: (originalText, replacedText) => {
dispatch(replaceText(originalText, replacedText));
},
};
};
Dashboard = connect(
mapStateToProps,
mapDispatchToProps,
)(Dashboard);
export default Dashboard;

View File

@@ -1,75 +0,0 @@
import React, { Component } from 'react';
import Auth from '../../Utils/Auth';
export default class Login extends Component {
static propTypes = {
location: React.PropTypes.object,
}
static contextTypes = {
router: React.PropTypes.object.isRequired,
}
state = {
email: '',
password: '',
error: null,
}
handleEmailChange = (event) => {
this.setState({ email: event.target.value });
}
handlePasswordChange = (event) => {
this.setState({ password: event.target.value });
}
handleSubmit = (event) => {
event.preventDefault();
Auth.login(this.state.email, this.state.password)
.then(() => {
const { location } = this.props;
if (location.state && location.state.nextPathname) {
this.context.router.replace(location.state.nextPathname);
} else {
this.context.router.replace('/dashboard');
}
})
.catch((err) => {
this.setState({ error: err.error });
});
}
render() {
return (
<div>
<h2>Login</h2>
<form action="" onSubmit={ this.handleSubmit }>
{this.state.error && (
<p>{ this.state.error }</p>
)}
<div>
<input
placeholder={ 'Email' }
onChange={ this.handleEmailChange }
/>
</div>
<div>
<input
placeholder={ 'Password' }
type={ 'password' }
onChange={ this.handlePasswordChange }
/>
</div>
<div>
<input type={ 'submit' } />
</div>
</form>
</div>
);
}
}

View File

@@ -1,2 +0,0 @@
import Login from './Login';
export default Login;

View File

@@ -0,0 +1,5 @@
import makeActionCreator from '../utils/actions';
export const TOGGLE_PREVIEW = 'TOGGLE_PREVIEW';
const togglePreview = makeActionCreator(TOGGLE_PREVIEW);

View File

@@ -0,0 +1,34 @@
import makeActionCreator from '../utils/actions';
import { push } from 'react-router-redux';
import { client } from 'utils/ApiClient';
import auth from 'utils/auth';
import { updateUser } from './UserActions';
import { updateTeam } from './TeamActions';
export const SLACK_AUTH_PENDING = 'SLACK_AUTH_PENDING';
export const SLACK_AUTH_SUCCESS = 'SLACK_AUTH_SUCCESS';
export const SLACK_AUTH_FAILURE = 'SLACK_AUTH_FAILURE';
const slackAuthPending = makeActionCreator(SLACK_AUTH_PENDING);
const slackAuthSuccess = makeActionCreator(SLACK_AUTH_SUCCESS, 'user');
const slackAuthFailure = makeActionCreator(SLACK_AUTH_FAILURE, 'error');
export function slackAuthAsync(code) {
return (dispatch) => {
dispatch(slackAuthPending());
client.post('/auth.slack', {
code: code,
})
.then(data => {
auth.setToken(data.data.accessToken);
dispatch(updateUser(data.data.user));
dispatch(updateTeam(data.data.team));
dispatch(push('/dashboard'));
})
// .catch((err) => {
// dispatch(push('/error'));
// })
};
};

View File

@@ -0,0 +1,5 @@
import makeActionCreator from '../utils/actions';
export const UPDATE_TEAM = 'UPDATE_TEAM';
export const updateTeam = makeActionCreator(UPDATE_TEAM, 'team');

View File

@@ -0,0 +1,5 @@
import makeActionCreator from '../utils/actions';
export const UPDATE_USER = 'UPDATE_USER';
export const updateUser = makeActionCreator(UPDATE_USER, 'user');

View File

View File

@@ -0,0 +1,2 @@
import Button from './Button';
export default Button;

View File

@@ -0,0 +1,60 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import MarkdownEditor from '../../components/MarkdownEditor';
import {
updateText,
replaceText,
} from '../../actions';
import constants from '../../constants';
import styles from './Editor.scss';
class Editor extends Component {
static propTypes = {
editMarkdown: React.PropTypes.func.isRequired,
text: React.PropTypes.string,
replaceText: React.PropTypes.func.isRequired,
showHistorySidebar: React.PropTypes.bool.isRequired,
}
render() {
return (
<div className={styles.container}>
<div className={ styles.markdown }>
<MarkdownEditor
onChange={this.props.editMarkdown}
text={this.props.text}
replaceText={this.props.replaceText}
/>
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
text: state.text.text,
editor: state.editor,
showHistorySidebar: state.historySidebar.visible,
revisions: state.text.revisions,
};
};
const mapDispatchToProps = (dispatch) => {
return {
editMarkdown: (text) => {
dispatch(updateText(text, 'markdown'));
},
};
};
Editor = connect(
mapStateToProps,
mapDispatchToProps,
)(Editor);
export default Editor;

View File

@@ -0,0 +1,2 @@
import Editor from './Editor';
export default Editor;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { connect } from 'react-redux';
import styles from './Layout.scss';
class Layout extends React.Component {
static propTypes = {
}
render() {
return (
<div className={ styles.container }>
<div className={ styles.header }>
<div className={ styles.teamName }>Coinbase</div>
<div className={ styles.user }>
<img src={ this.props.avatarUrl } />
</div>
</div>
<div className={ styles.content }>
{ this.props.children }
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
avatarUrl: state.user ? state.user.avatarUrl : null,
}
};
export default connect(mapStateToProps)(Layout);

View File

@@ -0,0 +1,35 @@
.container {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 42px;
border-bottom: 1px solid #eee;
}
.teamName {
font-family: 'Atlas Grotesk';
font-weight: bold;
}
.user {
img {
height: 24px;
width: 24px;
border-radius: 12px;
}
}
.content {
flex: 1;
}

View File

@@ -0,0 +1,2 @@
import Layout from './Layout';
export default Layout;

View File

@@ -0,0 +1,66 @@
/*
Name: Base16 Default Light
Author: Chris Kempson (http://chriskempson.com)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
:global {
/* Custom styling */
.cm-s-atlas.CodeMirror {
background: #ffffff;
color: #202020;
font-family: 'Atlas Typewriter', 'Cousine', 'Monaco', monospace;
font-weight: 300;
height: auto;
}
.cm-s-atlas div.CodeMirror-selected {
background: #90CAF9;
}
/* Disable ondrag cursor for file uploads */
.cm-s-atlas div.CodeMirror-dragcursors {
visibility: hidden;
}
.cm-s-atlas .CodeMirror-line::selection,
.cm-s-atlas .CodeMirror-line > span::selection,
.cm-s-atlas .CodeMirror-line > span > span::selection {
background: #90CAF9;
}
.cm-s-atlas .CodeMirror-line::-moz-selection, .cm-s-atlas .CodeMirror-line > span::-moz-selection, .cm-s-atlas .CodeMirror-line > span > span::-moz-selection { background: #e0e0e0; }
.cm-s-atlas .CodeMirror-gutters { background: #f5f5f5; border-right: 0px; }
.cm-s-atlas .CodeMirror-guttermarker { color: #ac4142; }
.cm-s-atlas .CodeMirror-guttermarker-subtle { color: #b0b0b0; }
.cm-s-atlas .CodeMirror-linenumber { color: #b0b0b0; }
.cm-s-atlas .CodeMirror-cursor {
border-left: 2px solid #2196F3;
}
.cm-s-atlas span.cm-quote {
font-style: italic;
}
.cm-s-atlas span.cm-comment { color: #8f5536; }
.cm-s-atlas span.cm-atom { color: #aa759f; }
.cm-s-atlas span.cm-number { color: #aa759f; }
.cm-s-atlas span.cm-property, .cm-s-atlas span.cm-attribute { color: #90a959; }
.cm-s-atlas span.cm-keyword { color: #ac4142; }
.cm-s-atlas span.cm-string { color: #f4bf75; }
.cm-s-atlas span.cm-variable { color: #90a959; }
.cm-s-atlas span.cm-variable-2 { color: #788696; }
.cm-s-atlas span.cm-def { color: #d28445; }
.cm-s-atlas span.cm-bracket { color: #202020; }
.cm-s-atlas span.cm-tag { color: #ac4142; }
.cm-s-atlas span.cm-link { color: #aa759f; }
.cm-s-atlas span.cm-error { background: #ac4142; color: #505050; }
.cm-s-atlas .CodeMirror-activeline-background { background: #DDDCDC; }
.cm-s-atlas .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }
.cm-s-atlas .CodeMirror-placeholder { color: rgba(0, 0, 0, 0.5); font-weight: bold; }
}

View File

View File

@@ -0,0 +1,2 @@
import Preview from './Preview';
export default Preview;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import styles from './SlackAuthLink.scss';
export default class SlackAuthLink extends React.Component {
static propTypes = {
scopes: React.PropTypes.arrayOf(React.PropTypes.string),
}
state = {
oauthState: Math.random().toString(36).substring(7),
}
static defaultProps = {
scopes: ['identify']
}
componentDidMount = () => {
localStorage.oauthState = this.state.oauthState;
}
slackUrl = () => {
const baseUrl = 'https://slack.com/oauth/authorize';
const params = {
client_id: '30086650419.30130733398',
scope: this.props.scopes.join(" "),
redirect_uri: 'http://localhost:3000/auth/slack/',
state: this.state.oauthState,
};
const urlParams = Object.keys(params).map(function(key) {
return key + '=' + encodeURIComponent(params[key]);
}).join('&');
return `${baseUrl}?${urlParams}`;
}
render() {
return (
<a href={ this.slackUrl() } className={ styles.link }>Authorize /w Slack</a>
)
}
}

View File

@@ -0,0 +1,6 @@
.link {
text-decoration: none;
background: no-repeat left/10% url(./assets/slack_icon.svg);
padding: 5px 0 4px 36px;
font-size: 1.4em;
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="148px" height="147px" viewBox="0 0 148 147" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs></defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<g>
<g>
<path d="M12.997,77.78 C7.503,77.822 2.849,74.548 1.133,69.438 C1.067,69.24 1.01,69.048 0.955,68.855 C-0.915,62.311 2.711,55.465 9.21,53.273 L113.45,18.35 C114.717,17.987 115.993,17.802 117.257,17.794 C122.897,17.75 127.679,21.096 129.437,26.314 L129.593,26.818 C131.543,33.634 126.698,39.718 120.893,41.668 C120.889,41.671 119.833,42.028 17.231,77.059 C15.844,77.53 14.421,77.768 12.997,77.78 L12.997,77.78 L12.997,77.78 Z" fill="#70CADB"></path>
<path d="M30.372,129.045 C24.835,129.085 20.165,125.857 18.469,120.82 C18.405,120.628 18.344,120.435 18.289,120.241 C16.393,113.619 20.015,106.701 26.536,104.506 L130.78,69.263 C132.127,68.813 133.518,68.583 134.917,68.57 C140.469,68.528 145.347,71.92 147.068,77.014 L147.228,77.544 C148.235,81.065 147.64,85.022 145.638,88.145 C144.146,90.467 139.44,92.511 139.44,92.511 L34.8,128.29 C33.342,128.777 31.855,129.034 30.372,129.047 L30.372,129.045 L30.372,129.045 Z" fill="#E01765"></path>
<path d="M117.148,129.268 C111.588,129.311 106.665,125.803 104.893,120.545 L70.103,17.205 L69.929,16.625 C68.044,10.035 71.669,3.161 78.166,0.971 C79.466,0.534 80.81,0.306 82.163,0.294 C84.173,0.279 86.118,0.732 87.95,1.637 C91.013,3.162 93.304,5.787 94.399,9.029 L129.186,112.36 L129.287,112.692 C131.241,119.534 127.624,126.412 121.127,128.602 C119.84,129.031 118.5,129.256 117.148,129.268 L117.148,129.268 L117.148,129.268 Z" fill="#E8A723"></path>
<path d="M65.435,146.674 C59.875,146.717 54.948,143.209 53.175,137.944 L18.394,34.608 C18.334,34.418 18.274,34.228 18.216,34.033 C16.336,27.445 19.95,20.57 26.445,18.378 C27.74,17.948 29.079,17.721 30.43,17.71 C35.991,17.666 40.915,21.173 42.687,26.433 L77.469,129.773 C77.534,129.953 77.593,130.152 77.646,130.342 C79.53,136.935 75.914,143.814 69.409,146.006 C68.117,146.437 66.78,146.662 65.431,146.673 L65.435,146.673 L65.435,146.674 Z" fill="#3EB890"></path>
<path d="M99.997,105.996 L124.255,97.702 L116.325,74.152 L92.039,82.359 L99.997,105.996 L99.997,105.996 Z" fill="#CC2027"></path>
<path d="M48.364,123.65 L72.62,115.357 L64.63,91.627 L40.35,99.837 L48.364,123.65 L48.364,123.65 Z" fill="#361238"></path>
<path d="M82.727,54.7 L106.987,46.417 L99.15,23.142 L74.845,31.285 L82.727,54.7 L82.727,54.7 Z" fill="#65863A"></path>
<path d="M31.088,72.33 L55.348,64.047 L47.415,40.475 L23.11,48.617 L31.088,72.33 L31.088,72.33 Z" fill="#1A937D"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,2 @@
import SlackAuthLink from './SlackAuthLink';
export default SlackAuthLink;

View File

@@ -1,40 +1,46 @@
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, Route } from 'react-router';
import { Router, Route, IndexRoute } from 'react-router';
import { createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import { persistStore, autoRehydrate } from 'redux-persist';
import thunkMiddleware from 'redux-thunk';
import * as storage from 'redux-storage';
import createEngine from 'redux-storage-engine-localstorage';
import History from './Utils/History';
import createLogger from 'redux-logger';
import History from 'utils/History';
import Auth from './Utils/Auth';
import auth from 'utils/auth';
import reducers from './Reducers';
import reducers from 'reducers';
import App from './Views/App';
import Login from './Views/Login';
import Dashboard from './Views/Dashboard';
import 'utils/base-styles.scss';
import Home from 'scenes/Home';
import App from 'scenes/App';
import Dashboard from 'scenes/Dashboard';
import SlackAuth from 'scenes/SlackAuth';
// Redux
const reducer = storage.reducer(reducers);
const engine = createEngine('atlas-store');
const storageMiddleware = storage.createMiddleware(engine);
const loggerMiddleware = createLogger();
const routerMiddlewareWithHistory = routerMiddleware(History);
const createStoreWithMiddleware = applyMiddleware(storageMiddleware)(createStore);
const store = createStoreWithMiddleware(reducer);
const createStoreWithMiddleware = (createStore);
const load = storage.createLoader(engine);
load(store);
// .then((newState) => console.log('Loaded state:', newState));
// .catch(() => console.log('Failed to load previous state'));
// React router
const store = createStore(reducer, applyMiddleware(
thunkMiddleware,
routerMiddlewareWithHistory,
loggerMiddleware,
), autoRehydrate());
persistStore(store);
function requireAuth(nextState, replace) {
if (!Auth.loggedIn()) {
if (!auth.loggedIn()) {
replace({
pathname: '/login',
pathname: '/',
state: { nextPathname: nextState.location.pathname },
});
}
@@ -43,7 +49,14 @@ function requireAuth(nextState, replace) {
render((
<Provider store={store}>
<Router history={History}>
<Route path="/" component={App} />
<Route path="/">
<IndexRoute component={Home} />
<Route path="/dashboard" component={Dashboard} onEnter={ requireAuth } />
<Route path="/editor" component={App} />
<Route path="/auth/slack" component={SlackAuth} />
</Route>
</Router>
</Provider>
), document.getElementById('root'));

View File

@@ -0,0 +1,62 @@
const defaultTest = `# Welcome to Beautiful Atlas
This is just a small preview here's what you can do:
- Write markdown or rich text, you choose
- Dont' worry about saving
- One document for now
- More to come
`
const state = {
previousTest: null,
text: null,
unsavedChanges: false,
};
const text = (state = state, action) => {
switch (action.type) {
case UPDATE_TEXT: {
let unsavedChanges = false;
if (lastRevision && lastRevision.text !== state.text) {
unsavedChanges = true;
}
return {
...state,
unsavedChanges,
text: action.text,
};
}
case ADD_REVISION: {
// Create new revision if it differs from the previous one
if (!lastRevision || lastRevision.text !== state.text) {
const lastId = lastRevision ? lastRevision.id : 0;
return {
...state,
revisions: [
...state.revisions,
{
id: lastId + 1,
text: state.text,
created_at: action.createdAt,
},
],
unsavedChanges: false,
};
} else {
return state;
}
}
case REPLACE_TEXT: {
const newText = state.text.replace(action.originalText, action.replacedText);
return {
...state,
text: newText,
};
}
default:
return state;
}
};

15
src/reducers/team.js Normal file
View File

@@ -0,0 +1,15 @@
import { UPDATE_TEAM } from 'actions/TeamActions';
const team = (state = null, action) => {
switch (action.type) {
case UPDATE_TEAM: {
return {
...action.team,
};
}
default:
return state;
}
};
export default team;

15
src/reducers/user.js Normal file
View File

@@ -0,0 +1,15 @@
import { UPDATE_USER } from 'actions/UserActions';
const user = (state = null, action) => {
switch (action.type) {
case UPDATE_USER: {
return {
...action.user,
};
}
default:
return state;
}
};
export default user;

View File

@@ -8,52 +8,28 @@ import styles from './App.scss';
import {
toggleEditors,
addRevision,
} from '../../Actions';
} from '../../actions';
import Header from '../../Components/Header';
import Dashboard from '../Dashboard';
import Auth from '../../Utils/Auth';
import Header from '../../components/Header';
import Editor from '../../components/Editor';
class App extends Component {
static propTypes = {
children: React.PropTypes.element,
activeEditors: React.PropTypes.array.isRequired,
toggleEditors: React.PropTypes.func.isRequired,
addRevision: React.PropTypes.func.isRequired,
unsavedChanges: React.PropTypes.bool.isRequired,
}
state = {
loggedIn: Auth.loggedIn(),
}
componentWillMount = () => {
Auth.onChange = this.updateAuth;
}
updateAuth = (loggedIn) => {
this.setState({
loggedIn,
});
}
logout = () => {
// TODO: Replace with Redux actions
Auth.logout();
}
render() {
return (
<div className={ styles.container }>
<Header
activeEditors={this.props.activeEditors}
toggleEditors={this.props.toggleEditors}
addRevision={this.props.addRevision}
unsavedChanges={this.props.unsavedChanges}
/>
<div className={ styles.content }>
<Dashboard />
<Editor />
</div>
</div>
);
@@ -69,9 +45,6 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => {
return {
toggleEditors: (toggledEditor) => {
dispatch(toggleEditors(toggledEditor));
},
addRevision: () => {
dispatch(addRevision());
},
@@ -84,9 +57,3 @@ App = connect(
)(App);
export default App;
// {this.state.loggedIn ? (
// <a href="#" onClick={this.logout}>Logout</a>
// ) : (
// <Link to="/login">Login</Link>
// )}

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { replace } from 'react-router-redux';
import { client } from 'utils/ApiClient';
import Layout from 'components/Layout';
import styles from './Dashboard.scss';
class Dashboard extends React.Component {
static propTypes = {
replace: React.PropTypes.func.isRequired,
}
render() {
return (
<Layout
header={<div>header!</div>}
>
holla!
</Layout>
);
}
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ replace }, dispatch)
}
export default connect(
null, mapDispatchToProps
)(Dashboard);

View File

View File

@@ -1,2 +1,2 @@
import Dashboard from './Dashboard';
export default Dashboard;
export default Dashboard;

63
src/scenes/Home/Home.js Normal file
View File

@@ -0,0 +1,63 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { replace } from 'react-router-redux';
import auth from '../../utils/auth';
import SlackAuthLink from '../../components/SlackAuthLink';
import styles from './Home.scss';
class Home extends React.Component {
static propTypes = {
replace: React.PropTypes.func.isRequired,
}
componentDidMount = () => {
if (auth.loggedIn()) {
this.props.replace('/dashboard');
}
}
render() {
return (
<div className={ styles.container }>
<div className={ styles.content }>
<div className={ styles.intro }>
<p>
Hi there,
</p>
<p>
We're building the best place for engineers, designers and teams to
share ideas, tell stories and build knowledge.
</p>
<p>
<strong>**Atlas**</strong> can start as a wiki, but it's really
up to you what you want to make of it:
</p>
<p>
- Write documentation in <i>_markdown_</i><br/>
- Build a blog around the API<br/>
- Hack the frontend for your needs (coming!)<br/>
</p>
<p>
We're just getting started.
</p>
</div>
<div className={ styles.action }>
<SlackAuthLink />
</div>
</div>
</div>
);
}
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ replace }, dispatch)
}
export default connect(
null, mapDispatchToProps
)(Home);

17
src/scenes/Home/Home.scss Normal file
View File

@@ -0,0 +1,17 @@
.container {
display: flex;
flex: 1;
justify-content: space-around;
}
.content {
margin: 40px;
max-width: 640px;
}
.intro {
font-family: "Atlas Typewriter", Monaco, monospace;
font-size: 1.4em;
line-height: 1.6em;
margin-bottom: 40px;
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Frame } from 'react-keyframes';
let frames = [];
const p = (node) => frames.push(node);
const E = (props) => {
return (<Frame duration={props.duration || 300} component='div'>{ props.children }</Frame>);
};
const line1 = (<p>Hi there,</p>);
const line2 = (<p>We're excited to share what were building.</p>);
const line3 = (<p>We <strong>**love**</strong> Markdown,</p>);
const line4 = (<p>but we also get that it's not for everyone.</p>);
const line5 = (<p>Together with you,</p>);
const line6 = (<p>we want to build the best place to</p>);
const line7 = (<p>share ideas,</p>);
const line8 = (<p>tell stories,</p>);
const line9 = (<p>and build knowledge.</p>);
const line10 = (<p>We're just getting started.</p>);
const line11 = (<p>Welcome to Beautiful Atlas.</p>);
p(<E>{line1}{line2}{line3}{line4}{line5}{line6}{line7}{line8}{line9}{line10}{line11}</E>);
// Hmms leaving this here for now, would be nice to something
export default frames;

2
src/scenes/Home/index.js Normal file
View File

@@ -0,0 +1,2 @@
import Home from './Home';
export default Home;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { slackAuthAsync } from '../../actions/SlackAuthAction';
import { client } from '../../utils/ApiClient';
class SlackAuth extends React.Component {
componentDidMount = () => {
const { query } = this.props.location
// Validate OAuth2 state param
if (localStorage.oauthState != query.state) {
return;
}
this.props.slackAuthAsync(query.code);
}
render() {
return (
<div>Loading...</div>
);
}
}
const mapDispactcToProps = (dispatch) => {
return bindActionCreators({ slackAuthAsync }, dispatch);
};
export default connect(
null,
mapDispactcToProps
)(SlackAuth);

View File

@@ -0,0 +1,2 @@
import SlackAuth from './SlackAuth';
export default SlackAuth;

9
src/utils/actions.js Normal file
View File

@@ -0,0 +1,9 @@
export default (type, ...argNames) => {
return function(...args) {
let action = { type }
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
})
return action
}
}

View File

@@ -0,0 +1,23 @@
* {
box-sizing: border-box;
}
html, body, .viewport {
width: 100%;
height: 100%;
margin: 0;
}
html, body {
font-family: 'Atlas Grotesk', 'Helvetica Neue', sans-serif;
}
a {
color: #0C77F8;
}
:global {
.Codemirror {
height: auto;
}
}