Skip to content

Commit

Permalink
feat(experiences): stub out main mechanisms for experience creation
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickocoffeyo committed Jun 6, 2018
1 parent eb54748 commit 0e347c0
Show file tree
Hide file tree
Showing 14 changed files with 399 additions and 9 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ globals:
parser: babel-eslint
rules:
strict: 0
# Disable this for now. Drupal does not use camel case properties and to make
# code more simple, we are using Drupal's field names in forms, etc.
camelcase: 0
react/jsx-filename-extension: [1, { "extensions": [".js", ".jsx"] }]
import/prefer-default-export: [0]
38 changes: 36 additions & 2 deletions src/actions/experiences.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

import { call, put, takeLatest } from 'redux-saga/effects';

import { EXPERIENCES_FETCH_FOR_USER } from '../constants';
import { experiencesFetchForUser as getExperiencesForUser } from '../lib/api';
import { EXPERIENCES_FETCH_FOR_USER, EXPERIENCES_CREATE } from '../constants';
import {
experiencesFetchForUser as getExperiencesForUser,
experiencesCreate as postExperiences
} from '../lib/api';
import actionGenerator from '../lib/actionGenerator';

/**
Expand Down Expand Up @@ -34,6 +37,37 @@ export function* experiencesFetchForUser({ user }) {
);
}

/**
* Creates a new experience.
*
* @param {object} payload - Payload for this saga action.
* @param {string} payload.title - Title of the new experience.
* @param {string} payload.field_experience_path - URL slug for new experience.
* @param {object} payload.user - Object containing user data.
* @param {object} payload.user.authentication - Object containing auth data.
* @param {string} payload.user.authentication.accessToken
* Access token for the current user.
* @param {string} payload.user.authentication.csrfToken
* CSRF token for the current user.
*/
export function* experiencesCreate({ user, title, field_experience_path }) {
yield* actionGenerator(
EXPERIENCES_CREATE,
function* experienceCreateHandler() {
const experience = yield call(
postExperiences,
{ title, field_experience_path },
user
);
yield put({
type: `${EXPERIENCES_CREATE}_SUCCESS`,
payload: experience
});
}
);
}

export function* watchExperiencesActions() {
yield takeLatest(EXPERIENCES_FETCH_FOR_USER, experiencesFetchForUser);
yield takeLatest(EXPERIENCES_CREATE, experiencesCreate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @file ExperienceCreateForm.container.js
* Exports a redux-connected ExperienceCreateForm component.
*/

import { connect } from 'react-redux';

import ExperienceCreateForm from './ExperienceCreateForm';

const mapDispatchToProps = dispatch => ({
dispatch
});

const mapState = ({ user, experiences }) => ({
user,
experiences
});

export default connect(mapState, mapDispatchToProps)(ExperienceCreateForm);
97 changes: 97 additions & 0 deletions src/components/ExperienceCreateForm/ExperienceCreateForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @file ExperienceCreateForm.js
* Exports a component that allows users to create an EditVR experience.
*/

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { TextField, Button, withStyles } from '@material-ui/core';

import { Message } from '../';
import ExperienceCreateFormStyles from './ExperienceCreateForm.style';
import { EXPERIENCES_CREATE } from '../../constants';

class ExperienceCreateForm extends Component {
static propTypes = {
submitHandler: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
classes: PropTypes.shape({
textField: PropTypes.string.isRequired,
button: PropTypes.string.isRequired
}).isRequired,
dispatch: PropTypes.func.isRequired,
user: PropTypes.shape({
authentication: PropTypes.shape({
accessToken: PropTypes.string.isRequired,
csrfToken: PropTypes.string.isRequired
}).isRequired
}).isRequired,
experiences: PropTypes.shape({
error: PropTypes.string
})
};

static defaultProps = {
submitHandler: false,
experiences: {
error: null
}
};

/**
* Handles a login form submit action.
*
* @param {object} event - Submit event object.
*/
handleSubmit = event => {
event.preventDefault();
const { dispatch, user } = this.props;
dispatch({
type: EXPERIENCES_CREATE,
user,
title: event.target[0].value,
field_experience_path: event.target[1].value
});
};

/**
* {@inheretdoc}
*/
render() {
const {
classes,
submitHandler,
experiences: { error }
} = this.props;
return (
<form onSubmit={submitHandler || this.handleSubmit}>
{error && <Message>{error}</Message>}
<TextField
id="title"
label="Title"
type="text"
required
helperText="Enter a user-friendly title for your experience."
className={classes.textField}
/>
<TextField
id="field_experience_path"
label="URL Path"
type="text"
required
helperText="Enter a name for your experience. For example, if you enter 'my-new-experience', your experience will be published to /experience/my-new-experience."
className={classes.textField}
/>
<Button
variant="raised"
color="primary"
type="submit"
className={classes.button}
>
Create
</Button>
</form>
);
}
}

export default withStyles(ExperienceCreateFormStyles)(ExperienceCreateForm);
15 changes: 15 additions & 0 deletions src/components/ExperienceCreateForm/ExperienceCreateForm.style.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @file ExperienceCreateForm.style.js
* Exports ExperienceCreateForm component styles.
*/

export default theme => ({
textField: {
width: '100%',
marginTop: `${theme.spacing.unit * 2}px`
},
button: {
marginTop: `${theme.spacing.unit * 3}px`,
marginRight: theme.spacing.unit
}
});
3 changes: 2 additions & 1 deletion src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

import App from './App/App';
import LoginForm from './LoginForm/LoginForm.container';
import ExperienceCreateForm from './ExperienceCreateForm/ExperienceCreateForm.container';
import Loading from './Loading/Loading';
import Message from './Message/Message';

export { App, LoginForm, Loading, Message };
export { App, LoginForm, ExperienceCreateForm, Loading, Message };
1 change: 1 addition & 0 deletions src/constants/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
export const API_ENDPOINT_USER_LOGIN = 'oauth/token';
export const API_ENDPOINT_XCSRF_TOKEN = 'rest/session/token';
export const API_ENDPOINT_EXPERIENCES = 'node/experience';
export const API_TYPE_EXPERIENCES = 'node--experiences';
1 change: 1 addition & 0 deletions src/constants/experiences.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
*/

export const EXPERIENCES_FETCH_FOR_USER = 'EXPERIENCES_FETCH_FOR_USER';
export const EXPERIENCES_CREATE = 'EXPERIENCES_CREATE';
32 changes: 31 additions & 1 deletion src/lib/api/experiences.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

import { clientId } from '../../config';
import axiosInstance from './axiosInstance';
import { API_ENDPOINT_EXPERIENCES } from '../../constants';
import {
API_ENDPOINT_EXPERIENCES,
API_TYPE_EXPERIENCES
} from '../../constants';

/**
* Fetches a list of experiences that are owned by the current user.
Expand All @@ -25,3 +28,30 @@ export const experiencesFetchForUser = async ({ uid, authentication }) =>
_consumer_id: clientId
}
});

/**
* Takes new experience data and POSTs it to the API.
*
* @param {object} experience - Object containing data for new experience.
* @param {string} experience.title - Title of the new experience.
* @param {string} experience.field_experience_path - URL slug for new experience.
* @param {object} user - Object containing information about the current user.
* @param {object} user.authentication - Object containing auth data.
* @param {string} user.authentication.accessToken
* Access token for the current user.
* @param {string} user.authentication.csrfToken
* CSRF token for the current user.
*/
export const experiencesCreate = async (
{ title, field_experience_path },
{ authentication }
) =>
axiosInstance(authentication).post(API_ENDPOINT_EXPERIENCES, {
data: {
type: API_TYPE_EXPERIENCES,
attributes: {
title,
field_experience_path
}
}
});
10 changes: 8 additions & 2 deletions src/lib/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

import axiosInstance from './axiosInstance';
import { getAccessToken, getCsrfToken } from './user';
import { experiencesFetchForUser } from './experiences';
import { experiencesFetchForUser, experiencesCreate } from './experiences';

export { axiosInstance, getAccessToken, getCsrfToken, experiencesFetchForUser };
export {
axiosInstance,
getAccessToken,
getCsrfToken,
experiencesFetchForUser,
experiencesCreate
};
7 changes: 6 additions & 1 deletion src/pages/ExperienceCreate/ExperienceCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import React from 'react';

import { DashboardLayout } from '../../layouts';
import { ExperienceCreateForm } from '../../components';

const ExperienceCreate = () => <DashboardLayout title="Create an Experience" />;
const ExperienceCreate = () => (
<DashboardLayout title="Create an Experience">
<ExperienceCreateForm />
</DashboardLayout>
);

export default ExperienceCreate;
6 changes: 6 additions & 0 deletions src/pages/ExperienceCreate/ExperienceCreate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ describe('<ExperienceCreate />', () => {
const store = configureStore()({
loadingBar: {
default: 0
},
user: {
authentication: {
accessToken: 'token',
csrfToken: 'token'
}
}
});

Expand Down
Loading

0 comments on commit 0e347c0

Please sign in to comment.