diff --git a/server/api/atlases.js b/server/api/atlases.js new file mode 100644 index 00000000..747f074c --- /dev/null +++ b/server/api/atlases.js @@ -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; \ No newline at end of file diff --git a/server/api/index.js b/server/api/index.js index 14b55ed3..3e0e9d07 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -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. diff --git a/server/models/Atlas.js b/server/models/Atlas.js index 4476c403..a96ba6ca 100644 --- a/server/models/Atlas.js +++ b/server/models/Atlas.js @@ -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); diff --git a/server/models/User.js b/server/models/User.js index 40964152..9b8da5f4 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -23,6 +23,9 @@ const User = sequelize.define('user', { getJwtToken() { return JWT.sign({ id: this.id }, this.jwtSecret); }, + async getTeam() { + return this.team; + } }, indexes: [ { diff --git a/server/presenters.js b/server/presenters.js index e3abf59a..a9baab29 100644 --- a/server/presenters.js +++ b/server/presenters.js @@ -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(), + } +} \ No newline at end of file diff --git a/src/Reducers/index.js b/src/Reducers/index.js index c378891b..f8336382 100644 --- a/src/Reducers/index.js +++ b/src/Reducers/index.js @@ -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, }); diff --git a/src/actions/AtlasActions.js b/src/actions/AtlasActions.js new file mode 100644 index 00000000..346decbf --- /dev/null +++ b/src/actions/AtlasActions.js @@ -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)); + }) + }; +}; \ No newline at end of file diff --git a/src/components/AtlasPreview/AtlasPreview.js b/src/components/AtlasPreview/AtlasPreview.js new file mode 100644 index 00000000..1ebaa966 --- /dev/null +++ b/src/components/AtlasPreview/AtlasPreview.js @@ -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 ( +
+

{ this.props.data.name }

+
No documents. Why not create one?
+
+ ); + } +}; + +export default AtlasPreview; \ No newline at end of file diff --git a/src/components/AtlasPreview/AtlasPreview.scss b/src/components/AtlasPreview/AtlasPreview.scss new file mode 100644 index 00000000..8a464b83 --- /dev/null +++ b/src/components/AtlasPreview/AtlasPreview.scss @@ -0,0 +1,6 @@ +@import '../../utils/constants.scss'; + +.atlasLink { + text-decoration: none; + color: $textColor; +} \ No newline at end of file diff --git a/src/components/AtlasPreview/index.js b/src/components/AtlasPreview/index.js new file mode 100644 index 00000000..6b1ecbf5 --- /dev/null +++ b/src/components/AtlasPreview/index.js @@ -0,0 +1,2 @@ +import AtlasPreview from './AtlasPreview'; +export default AtlasPreview; \ No newline at end of file diff --git a/src/components/CenteredContent/CenteredContent.js b/src/components/CenteredContent/CenteredContent.js new file mode 100644 index 00000000..71dfdad0 --- /dev/null +++ b/src/components/CenteredContent/CenteredContent.js @@ -0,0 +1,27 @@ +import React from 'react'; + +import styles from './CenteredContent.scss'; + +const CenteredContent = (props) => { + const style = { + maxWidth: props.maxWidth, + ...props.style, + }; + + return ( +
+ { props.children } +
+ ); +}; + +CenteredContent.defaultProps = { + maxWidth: '600px', +}; + +CenteredContent.propTypes = { + children: React.PropTypes.node.isRequired, + style: React.PropTypes.object, +}; + +export default CenteredContent; \ No newline at end of file diff --git a/src/components/CenteredContent/CenteredContent.scss b/src/components/CenteredContent/CenteredContent.scss new file mode 100644 index 00000000..1e74499d --- /dev/null +++ b/src/components/CenteredContent/CenteredContent.scss @@ -0,0 +1,5 @@ +.content { + display: flex; + flex: 1; + margin: 40px 20px; +} \ No newline at end of file diff --git a/src/components/CenteredContent/index.js b/src/components/CenteredContent/index.js new file mode 100644 index 00000000..1e05cad5 --- /dev/null +++ b/src/components/CenteredContent/index.js @@ -0,0 +1,2 @@ +import CenteredContent from './CenteredContent'; +export default CenteredContent; \ No newline at end of file diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js index e0e07f5e..24c1408d 100644 --- a/src/components/Layout/Layout.js +++ b/src/components/Layout/Layout.js @@ -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 (
-
Coinbase
+
+ { this.props.teamName } +
@@ -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, } }; diff --git a/src/components/Layout/Layout.scss b/src/components/Layout/Layout.scss index bfa06a59..b5985188 100644 --- a/src/components/Layout/Layout.scss +++ b/src/components/Layout/Layout.scss @@ -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 { diff --git a/src/index.js b/src/index.js index 466f829d..c2b0bbd8 100644 --- a/src/index.js +++ b/src/index.js @@ -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, { - - - + + - + @@ -69,4 +68,3 @@ function requireAuth(nextState, replace) { }); } } - diff --git a/src/reducers/atlases.js b/src/reducers/atlases.js new file mode 100644 index 00000000..c46317ee --- /dev/null +++ b/src/reducers/atlases.js @@ -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; \ No newline at end of file diff --git a/src/scenes/Atlas/Atlas.js b/src/scenes/Atlas/Atlas.js new file mode 100644 index 00000000..b50b6476 --- /dev/null +++ b/src/scenes/Atlas/Atlas.js @@ -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 ( + + + { this.state.isLoading ? ( + + ) : ( +
+
+

{ data.name }

+
+ { data.description } +
+
+ +
+
+ ) } +
+
+ ); + } +} + +const mapStateToProps = (state) => { + return { + isLoading: state.atlases.isLoading, + } +}; + +const mapDispatchToProps = (dispatch) => { + return bindActionCreators({ + replace, + fetchAtlasAsync, + }, dispatch) +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Atlas); diff --git a/src/scenes/Atlas/Atlas.scss b/src/scenes/Atlas/Atlas.scss new file mode 100644 index 00000000..547b0d7b --- /dev/null +++ b/src/scenes/Atlas/Atlas.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/scenes/Atlas/index.js b/src/scenes/Atlas/index.js new file mode 100644 index 00000000..aa7713c8 --- /dev/null +++ b/src/scenes/Atlas/index.js @@ -0,0 +1,2 @@ +import Atlas from './Atlas'; +export default Atlas; \ No newline at end of file diff --git a/src/scenes/Dashboard/Dashboard.js b/src/scenes/Dashboard/Dashboard.js index e8d1403b..a6c9ade3 100644 --- a/src/scenes/Dashboard/Dashboard.js +++ b/src/scenes/Dashboard/Dashboard.js @@ -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 ( - header!
} - > -
- -
+ + + { this.props.isLoading ? ( + + ) : this.props.items.map((item) => { + return (); + }) } + ); } } +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); diff --git a/src/scenes/Dashboard/Dashboard.scss b/src/scenes/Dashboard/Dashboard.scss index a2dad1f8..e69de29b 100644 --- a/src/scenes/Dashboard/Dashboard.scss +++ b/src/scenes/Dashboard/Dashboard.scss @@ -1,7 +0,0 @@ -.content { - display: flex; - flex: 1; - max-width: 600px; - - margin: 40px 20px; -} \ No newline at end of file