Skip to content

Commit 0e347c0

Browse files
feat(experiences): stub out main mechanisms for experience creation
1 parent eb54748 commit 0e347c0

File tree

14 files changed

+399
-9
lines changed

14 files changed

+399
-9
lines changed

.eslintrc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@ globals:
1717
parser: babel-eslint
1818
rules:
1919
strict: 0
20+
# Disable this for now. Drupal does not use camel case properties and to make
21+
# code more simple, we are using Drupal's field names in forms, etc.
22+
camelcase: 0
2023
react/jsx-filename-extension: [1, { "extensions": [".js", ".jsx"] }]
2124
import/prefer-default-export: [0]

src/actions/experiences.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55

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

8-
import { EXPERIENCES_FETCH_FOR_USER } from '../constants';
9-
import { experiencesFetchForUser as getExperiencesForUser } from '../lib/api';
8+
import { EXPERIENCES_FETCH_FOR_USER, EXPERIENCES_CREATE } from '../constants';
9+
import {
10+
experiencesFetchForUser as getExperiencesForUser,
11+
experiencesCreate as postExperiences
12+
} from '../lib/api';
1013
import actionGenerator from '../lib/actionGenerator';
1114

1215
/**
@@ -34,6 +37,37 @@ export function* experiencesFetchForUser({ user }) {
3437
);
3538
}
3639

40+
/**
41+
* Creates a new experience.
42+
*
43+
* @param {object} payload - Payload for this saga action.
44+
* @param {string} payload.title - Title of the new experience.
45+
* @param {string} payload.field_experience_path - URL slug for new experience.
46+
* @param {object} payload.user - Object containing user data.
47+
* @param {object} payload.user.authentication - Object containing auth data.
48+
* @param {string} payload.user.authentication.accessToken
49+
* Access token for the current user.
50+
* @param {string} payload.user.authentication.csrfToken
51+
* CSRF token for the current user.
52+
*/
53+
export function* experiencesCreate({ user, title, field_experience_path }) {
54+
yield* actionGenerator(
55+
EXPERIENCES_CREATE,
56+
function* experienceCreateHandler() {
57+
const experience = yield call(
58+
postExperiences,
59+
{ title, field_experience_path },
60+
user
61+
);
62+
yield put({
63+
type: `${EXPERIENCES_CREATE}_SUCCESS`,
64+
payload: experience
65+
});
66+
}
67+
);
68+
}
69+
3770
export function* watchExperiencesActions() {
3871
yield takeLatest(EXPERIENCES_FETCH_FOR_USER, experiencesFetchForUser);
72+
yield takeLatest(EXPERIENCES_CREATE, experiencesCreate);
3973
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @file ExperienceCreateForm.container.js
3+
* Exports a redux-connected ExperienceCreateForm component.
4+
*/
5+
6+
import { connect } from 'react-redux';
7+
8+
import ExperienceCreateForm from './ExperienceCreateForm';
9+
10+
const mapDispatchToProps = dispatch => ({
11+
dispatch
12+
});
13+
14+
const mapState = ({ user, experiences }) => ({
15+
user,
16+
experiences
17+
});
18+
19+
export default connect(mapState, mapDispatchToProps)(ExperienceCreateForm);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @file ExperienceCreateForm.js
3+
* Exports a component that allows users to create an EditVR experience.
4+
*/
5+
6+
import React, { Component } from 'react';
7+
import PropTypes from 'prop-types';
8+
import { TextField, Button, withStyles } from '@material-ui/core';
9+
10+
import { Message } from '../';
11+
import ExperienceCreateFormStyles from './ExperienceCreateForm.style';
12+
import { EXPERIENCES_CREATE } from '../../constants';
13+
14+
class ExperienceCreateForm extends Component {
15+
static propTypes = {
16+
submitHandler: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
17+
classes: PropTypes.shape({
18+
textField: PropTypes.string.isRequired,
19+
button: PropTypes.string.isRequired
20+
}).isRequired,
21+
dispatch: PropTypes.func.isRequired,
22+
user: PropTypes.shape({
23+
authentication: PropTypes.shape({
24+
accessToken: PropTypes.string.isRequired,
25+
csrfToken: PropTypes.string.isRequired
26+
}).isRequired
27+
}).isRequired,
28+
experiences: PropTypes.shape({
29+
error: PropTypes.string
30+
})
31+
};
32+
33+
static defaultProps = {
34+
submitHandler: false,
35+
experiences: {
36+
error: null
37+
}
38+
};
39+
40+
/**
41+
* Handles a login form submit action.
42+
*
43+
* @param {object} event - Submit event object.
44+
*/
45+
handleSubmit = event => {
46+
event.preventDefault();
47+
const { dispatch, user } = this.props;
48+
dispatch({
49+
type: EXPERIENCES_CREATE,
50+
user,
51+
title: event.target[0].value,
52+
field_experience_path: event.target[1].value
53+
});
54+
};
55+
56+
/**
57+
* {@inheretdoc}
58+
*/
59+
render() {
60+
const {
61+
classes,
62+
submitHandler,
63+
experiences: { error }
64+
} = this.props;
65+
return (
66+
<form onSubmit={submitHandler || this.handleSubmit}>
67+
{error && <Message>{error}</Message>}
68+
<TextField
69+
id="title"
70+
label="Title"
71+
type="text"
72+
required
73+
helperText="Enter a user-friendly title for your experience."
74+
className={classes.textField}
75+
/>
76+
<TextField
77+
id="field_experience_path"
78+
label="URL Path"
79+
type="text"
80+
required
81+
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."
82+
className={classes.textField}
83+
/>
84+
<Button
85+
variant="raised"
86+
color="primary"
87+
type="submit"
88+
className={classes.button}
89+
>
90+
Create
91+
</Button>
92+
</form>
93+
);
94+
}
95+
}
96+
97+
export default withStyles(ExperienceCreateFormStyles)(ExperienceCreateForm);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @file ExperienceCreateForm.style.js
3+
* Exports ExperienceCreateForm component styles.
4+
*/
5+
6+
export default theme => ({
7+
textField: {
8+
width: '100%',
9+
marginTop: `${theme.spacing.unit * 2}px`
10+
},
11+
button: {
12+
marginTop: `${theme.spacing.unit * 3}px`,
13+
marginRight: theme.spacing.unit
14+
}
15+
});

src/components/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
import App from './App/App';
77
import LoginForm from './LoginForm/LoginForm.container';
8+
import ExperienceCreateForm from './ExperienceCreateForm/ExperienceCreateForm.container';
89
import Loading from './Loading/Loading';
910
import Message from './Message/Message';
1011

11-
export { App, LoginForm, Loading, Message };
12+
export { App, LoginForm, ExperienceCreateForm, Loading, Message };

src/constants/api.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
export const API_ENDPOINT_USER_LOGIN = 'oauth/token';
77
export const API_ENDPOINT_XCSRF_TOKEN = 'rest/session/token';
88
export const API_ENDPOINT_EXPERIENCES = 'node/experience';
9+
export const API_TYPE_EXPERIENCES = 'node--experiences';

src/constants/experiences.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
*/
55

66
export const EXPERIENCES_FETCH_FOR_USER = 'EXPERIENCES_FETCH_FOR_USER';
7+
export const EXPERIENCES_CREATE = 'EXPERIENCES_CREATE';

src/lib/api/experiences.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
import { clientId } from '../../config';
77
import axiosInstance from './axiosInstance';
8-
import { API_ENDPOINT_EXPERIENCES } from '../../constants';
8+
import {
9+
API_ENDPOINT_EXPERIENCES,
10+
API_TYPE_EXPERIENCES
11+
} from '../../constants';
912

1013
/**
1114
* Fetches a list of experiences that are owned by the current user.
@@ -25,3 +28,30 @@ export const experiencesFetchForUser = async ({ uid, authentication }) =>
2528
_consumer_id: clientId
2629
}
2730
});
31+
32+
/**
33+
* Takes new experience data and POSTs it to the API.
34+
*
35+
* @param {object} experience - Object containing data for new experience.
36+
* @param {string} experience.title - Title of the new experience.
37+
* @param {string} experience.field_experience_path - URL slug for new experience.
38+
* @param {object} user - Object containing information about the current user.
39+
* @param {object} user.authentication - Object containing auth data.
40+
* @param {string} user.authentication.accessToken
41+
* Access token for the current user.
42+
* @param {string} user.authentication.csrfToken
43+
* CSRF token for the current user.
44+
*/
45+
export const experiencesCreate = async (
46+
{ title, field_experience_path },
47+
{ authentication }
48+
) =>
49+
axiosInstance(authentication).post(API_ENDPOINT_EXPERIENCES, {
50+
data: {
51+
type: API_TYPE_EXPERIENCES,
52+
attributes: {
53+
title,
54+
field_experience_path
55+
}
56+
}
57+
});

src/lib/api/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
import axiosInstance from './axiosInstance';
77
import { getAccessToken, getCsrfToken } from './user';
8-
import { experiencesFetchForUser } from './experiences';
8+
import { experiencesFetchForUser, experiencesCreate } from './experiences';
99

10-
export { axiosInstance, getAccessToken, getCsrfToken, experiencesFetchForUser };
10+
export {
11+
axiosInstance,
12+
getAccessToken,
13+
getCsrfToken,
14+
experiencesFetchForUser,
15+
experiencesCreate
16+
};

0 commit comments

Comments
 (0)