Added more views and atlas APIs

This commit is contained in:
Jori Lallo 2016-05-07 11:52:08 -07:00
parent 84ba65f72a
commit cbe9c0b6ee
22 changed files with 397 additions and 27 deletions

52
server/api/atlases.js Normal file
View File

@ -0,0 +1,52 @@
import Router from 'koa-router';
import httpErrors from 'http-errors';
import auth from './authentication';
import pagination from './middlewares/pagination';
import { presentAtlas } from '../presenters';
import { Team, Atlas } from '../models';
const router = new Router();
router.post('atlases.info', auth(), async (ctx) => {
let { id } = ctx.request.body;
ctx.assertPresent(id, 'id is required');
const team = await ctx.state.user.getTeam();
const atlas = await Atlas.findOne({
where: {
id: id,
teamId: team.id,
},
});
if (!atlas) throw httpErrors.NotFound();
ctx.body = {
data: presentAtlas(atlas),
};
});
router.post('atlases.list', auth(), pagination(), async (ctx) => {
let { teamId } = ctx.request.body;
ctx.assertPresent(teamId, 'teamId is required');
const team = await ctx.state.user.getTeam();
const atlases = await Atlas.findAll({
where: {
teamId: teamId,
},
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
ctx.body = {
pagination: ctx.state.pagination,
data: atlases.map((atlas) => {
return presentAtlas(atlas);
})
};
});
export default router;

View File

@ -6,6 +6,7 @@ import Sequelize from 'sequelize';
import auth from './auth';
import user from './user';
import atlases from './atlases';
import validation from './validation';
@ -42,6 +43,7 @@ api.use(validation());
router.use('/', auth.routes());
router.use('/', user.routes());
router.use('/', atlases.routes());
// Router is embedded in a Koa application wrapper, because koa-router does not
// allow middleware to catch any routes which were not explicitly defined.

View File

@ -4,9 +4,19 @@ import {
} from '../sequelize';
import Team from './Team';
const allowedAtlasTypes = [['atlas', 'journal']];
const Atlas = sequelize.define('atlas', {
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
name: DataTypes.STRING,
description: DataTypes.STRING,
type: { type: DataTypes.STRING, validate: { isIn: allowedAtlasTypes }},
}, {
instanceMethods: {
getRecentDocuments() {
return [];
},
},
});
Atlas.belongsTo(Team);

View File

@ -23,6 +23,9 @@ const User = sequelize.define('user', {
getJwtToken() {
return JWT.sign({ id: this.id }, this.jwtSecret);
},
async getTeam() {
return this.team;
}
},
indexes: [
{

View File

@ -14,3 +14,13 @@ export function presentTeam(team) {
name: team.name,
};
}
export function presentAtlas(atlas) {
return {
id: atlas.id,
name: atlas.name,
description: atlas.description,
type: atlas.type,
recentDocuments: atlas.getRecentDocuments(),
}
}

View File

@ -106,6 +106,7 @@ const text = (state = textDefaultState, action) => {
import team from './team';
import user from './user';
import atlases from './atlases';
export default combineReducers({
activeEditors,
@ -113,4 +114,5 @@ export default combineReducers({
text,
user,
team,
atlases,
});

View File

@ -0,0 +1,52 @@
import makeActionCreator from '../utils/actions';
import { client } from 'utils/ApiClient';
export const FETCH_ATLASES_PENDING = 'FETCH_ATLASES_PENDING';
export const FETCH_ATLASES_SUCCESS = 'FETCH_ATLASES_SUCCESS';
export const FETCH_ATLASES_FAILURE = 'FETCH_ATLASES_FAILURE';
const fetchAtlasesPending = makeActionCreator(FETCH_ATLASES_PENDING);
const fetchAtlasesSuccess = makeActionCreator(FETCH_ATLASES_SUCCESS, 'items', 'pagination');
const fetchAtlasesFailure = makeActionCreator(FETCH_ATLASES_FAILURE, 'error');
export function fetchAtlasesAsync(teamId) {
return (dispatch) => {
dispatch(fetchAtlasesPending());
client.post('/atlases.list', {
teamId: teamId,
})
.then(data => {
dispatch(fetchAtlasesSuccess(data.data, data.pagination));
})
.catch((err) => {
dispatch(fetchAtlasesFailure(err));
})
};
};
export const FETCH_ATLAS_PENDING = 'FETCH_ATLAS_PENDING';
export const FETCH_ATLAS_SUCCESS = 'FETCH_ATLAS_SUCCESS';
export const FETCH_ATLAS_FAILURE = 'FETCH_ATLAS_FAILURE';
const fetchAtlasPending = makeActionCreator(FETCH_ATLAS_PENDING);
const fetchAtlasSuccess = makeActionCreator(FETCH_ATLAS_SUCCESS, 'data');
const fetchAtlasFailure = makeActionCreator(FETCH_ATLAS_FAILURE, 'error');
export function fetchAtlasAsync(atlasId) {
return (dispatch) => {
dispatch(fetchAtlasPending());
client.post('/atlases.info', {
id: atlasId,
})
.then(data => {
dispatch(fetchAtlasSuccess(data.data,));
})
.catch((err) => {
dispatch(fetchAtlasFailure(err));
})
};
};

View File

@ -0,0 +1,23 @@
import React from 'react';
import Link from 'react-router/lib/Link';
import styles from './AtlasPreview.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
class AtlasPreview extends React.Component {
static propTypes = {
data: React.PropTypes.object.isRequired,
}
render() {
return (
<div className={ styles.container }>
<h2><Link to={ `/atlas/${this.props.data.id}` } className={ styles.atlasLink }>{ this.props.data.name }</Link></h2>
<div>No documents. Why not <Link to='/new-document'>create one</Link>?</div>
</div>
);
}
};
export default AtlasPreview;

View File

@ -0,0 +1,6 @@
@import '../../utils/constants.scss';
.atlasLink {
text-decoration: none;
color: $textColor;
}

View File

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

View File

@ -0,0 +1,27 @@
import React from 'react';
import styles from './CenteredContent.scss';
const CenteredContent = (props) => {
const style = {
maxWidth: props.maxWidth,
...props.style,
};
return (
<div className={ styles.content } style={ style }>
{ props.children }
</div>
);
};
CenteredContent.defaultProps = {
maxWidth: '600px',
};
CenteredContent.propTypes = {
children: React.PropTypes.node.isRequired,
style: React.PropTypes.object,
};
export default CenteredContent;

View File

@ -0,0 +1,5 @@
.content {
display: flex;
flex: 1;
margin: 40px 20px;
}

View File

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

View File

@ -1,5 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import Link from 'react-router/lib/Link';
import HeaderMenu from './components/HeaderMenu';
@ -14,7 +15,9 @@ class Layout extends React.Component {
return (
<div className={ styles.container }>
<div className={ styles.header }>
<div className={ styles.teamName }>Coinbase</div>
<div className={ styles.teamName }>
<Link to="/">{ this.props.teamName }</Link>
</div>
<HeaderMenu>
<img src={ this.props.avatarUrl } />
</HeaderMenu>
@ -29,6 +32,7 @@ class Layout extends React.Component {
const mapStateToProps = (state) => {
return {
teamName: state.team ? state.team.name : null,
avatarUrl: state.user ? state.user.avatarUrl : null,
}
};

View File

@ -1,3 +1,5 @@
@import '../../utils/constants.scss';
.container {
display: flex;
flex: 1;
@ -16,9 +18,11 @@
border-bottom: 1px solid #eee;
}
.teamName {
.teamName a {
font-family: 'Atlas Grotesk';
font-weight: bold;
color: $textColor;
text-decoration: none;
}
.content {

View File

@ -22,6 +22,7 @@ import 'fonts/atlas/atlas.css';
import Home from 'scenes/Home';
// import App from 'scenes/App';
import Dashboard from 'scenes/Dashboard';
import Atlas from 'scenes/Atlas';
import SlackAuth from 'scenes/SlackAuth';
// Redux
@ -47,12 +48,10 @@ persistStore(store, {
<Route path="/">
<IndexRoute component={Home} />
<Route path="/dashboard" component={Dashboard
} onEnter={ requireAuth } />
<Route path="/atlas/:id" component={Dashboard} onEnter={ requireAuth } />
<Route path="/atlas/:id/new" component={Dashboard} onEnter={ requireAuth } />
<Route path="/dashboard" component={ Dashboard } onEnter={ requireAuth } />
<Route path="/atlas/:id" component={ Atlas } onEnter={ requireAuth } />
<Route path="/editor" component={Dashboard} />
<Route path="/editor" component={Dashboard} onEnter={ requireAuth } />
<Route path="/auth/slack" component={SlackAuth} />
</Route>
@ -69,4 +68,3 @@ function requireAuth(nextState, replace) {
});
}
}

41
src/reducers/atlases.js Normal file
View File

@ -0,0 +1,41 @@
import {
FETCH_ATLASES_PENDING,
FETCH_ATLASES_SUCCESS,
FETCH_ATLASES_FAILURE,
} from 'actions/AtlasActions';
const initialState = {
items: [],
pagination: null,
isLoading: false,
}
const atlases = (state = initialState, action) => {
switch (action.type) {
case FETCH_ATLASES_PENDING: {
return {
...state,
isLoading: true,
};
}
case FETCH_ATLASES_SUCCESS: {
return {
...state,
items: action.items,
pagination: action.pagination,
isLoading: false,
};
}
case FETCH_ATLASES_FAILURE: {
return {
...state,
isLoading: false,
error: action.error,
};
}
default:
return state;
}
};
export default atlases;

85
src/scenes/Atlas/Atlas.js Normal file
View File

@ -0,0 +1,85 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { replace } from 'react-router-redux';
import { fetchAtlasAsync } from 'actions/AtlasActions';
// Temp
import { client } from 'utils/ApiClient';
import Layout from 'components/Layout';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
import styles from './Atlas.scss';
class Atlas extends React.Component {
static propTypes = {
atlas: React.PropTypes.object,
}
state = {
isLoading: true,
data: null,
}
componentDidMount = () => {
const { id } = this.props.params;
// this.props.fetchAtlasAsync(id);
// Temp before breaking out redux store
client.post('/atlases.info', {
id: id,
})
.then(data => {
this.setState({
isLoading: false,
data: data.data
});
})
}
render() {
const data = this.state.data;
return (
<Layout>
<CenteredContent>
{ this.state.isLoading ? (
<AtlasPreviewLoading />
) : (
<div className={ styles.container }>
<div className={ styles.atlasDetails }>
<h2>{ data.name }</h2>
<blockquote>
{ data.description }
</blockquote>
</div>
<div className={ styles.divider }><span></span></div>
</div>
) }
</CenteredContent>
</Layout>
);
}
}
const mapStateToProps = (state) => {
return {
isLoading: state.atlases.isLoading,
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
replace,
fetchAtlasAsync,
}, dispatch)
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Atlas);

View File

@ -0,0 +1,31 @@
.container {
display: flex;
flex: 1;
flex-direction: column;
}
.atlasDetails {
display: flex;
flex: 1;
flex-direction: column;
blockquote {
padding: 0;
margin: 0 0 20px 0;
font-style: italic;
}
}
.divider {
display: flex;
flex: 1;
justify-content: center;
span {
display: flex;
width: 50%;
margin: 20px 0;
border-bottom: 1px solid #eee;
}
}

View File

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

View File

@ -1,37 +1,53 @@
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 { fetchAtlasesAsync } from 'actions/AtlasActions';
import Layout from 'components/Layout';
import AtlasPreview from 'components/AtlasPreview';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
import styles from './Dashboard.scss';
class Dashboard extends React.Component {
static propTypes = {
replace: React.PropTypes.func.isRequired,
}
componentDidMount = () => {
this.props.fetchAtlasesAsync(this.props.teamId);
}
render() {
return (
<Layout
header={<div>header!</div>}
>
<div className={ styles.content }>
<AtlasPreviewLoading />
</div>
<Layout>
<CenteredContent>
{ this.props.isLoading ? (
<AtlasPreviewLoading />
) : this.props.items.map((item) => {
return (<AtlasPreview data={ item } />);
}) }
</CenteredContent>
</Layout>
);
}
}
const mapStateToProps = (state) => {
return {
teamId: state.team ? state.team.id : null,
isLoading: state.atlases.isLoading,
items: state.atlases.items,
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ replace }, dispatch)
return bindActionCreators({
fetchAtlasesAsync,
}, dispatch)
}
export default connect(
null, mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(Dashboard);

View File

@ -1,7 +0,0 @@
.content {
display: flex;
flex: 1;
max-width: 600px;
margin: 40px 20px;
}