Skip to content

Commit 1a3a1aa

Browse files
authored
Merge pull request #1164 from andrewn/feature/sketch-collections
Sketch collections
2 parents 0d061a9 + 8087a3f commit 1a3a1aa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+3548
-293
lines changed

client/components/AddRemoveButton.jsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import InlineSVG from 'react-inlinesvg';
4+
5+
const addIcon = require('../images/plus.svg');
6+
const removeIcon = require('../images/minus.svg');
7+
8+
const AddRemoveButton = ({ type, onClick }) => {
9+
const alt = type === 'add' ? 'add to collection' : 'remove from collection';
10+
const icon = type === 'add' ? addIcon : removeIcon;
11+
12+
return (
13+
<button className="overlay__close-button" onClick={onClick}>
14+
<InlineSVG src={icon} alt={alt} />
15+
</button>
16+
);
17+
};
18+
19+
AddRemoveButton.propTypes = {
20+
type: PropTypes.oneOf(['add', 'remove']).isRequired,
21+
onClick: PropTypes.func.isRequired,
22+
};
23+
24+
export default AddRemoveButton;

client/components/Nav.jsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,19 @@ class Nav extends React.PureComponent {
326326
Open
327327
</Link>
328328
</li> }
329+
{__process.env.UI_COLLECTIONS_ENABLED &&
330+
this.props.user.authenticated &&
331+
this.props.project.id &&
332+
<li className="nav__dropdown-item">
333+
<Link
334+
to={`/${this.props.user.username}/sketches/${this.props.project.id}/add-to-collection`}
335+
onFocus={this.handleFocusForFile}
336+
onBlur={this.handleBlur}
337+
onClick={this.setDropdownForNone}
338+
>
339+
Add to Collection
340+
</Link>
341+
</li>}
329342
{ __process.env.EXAMPLES_ENABLED &&
330343
<li className="nav__dropdown-item">
331344
<Link
@@ -576,6 +589,18 @@ class Nav extends React.PureComponent {
576589
My sketches
577590
</Link>
578591
</li>
592+
{__process.env.UI_COLLECTIONS_ENABLED &&
593+
<li className="nav__dropdown-item">
594+
<Link
595+
to={`/${this.props.user.username}/collections`}
596+
onFocus={this.handleFocusForAccount}
597+
onBlur={this.handleBlur}
598+
onClick={this.setDropdownForNone}
599+
>
600+
My collections
601+
</Link>
602+
</li>
603+
}
579604
<li className="nav__dropdown-item">
580605
<Link
581606
to={`/${this.props.user.username}/assets`}

client/components/PreviewNav.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const logoUrl = require('../images/p5js-logo-small.svg');
77
const editorUrl = require('../images/code.svg');
88

99
const PreviewNav = ({ owner, project }) => (
10-
<nav className="nav">
10+
<nav className="nav preview-nav">
1111
<div className="nav__items-left">
1212
<div className="nav__item-logo">
1313
<InlineSVG src={logoUrl} alt="p5.js logo" />

client/constants.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ export const HIDE_EDIT_PROJECT_NAME = 'HIDE_EDIT_PROJECT_NAME';
3636
export const SET_PROJECT = 'SET_PROJECT';
3737
export const SET_PROJECTS = 'SET_PROJECTS';
3838

39+
export const SET_COLLECTIONS = 'SET_COLLECTIONS';
40+
export const CREATE_COLLECTION = 'CREATED_COLLECTION';
41+
export const DELETE_COLLECTION = 'DELETE_COLLECTION';
42+
43+
export const ADD_TO_COLLECTION = 'ADD_TO_COLLECTION';
44+
export const REMOVE_FROM_COLLECTION = 'REMOVE_FROM_COLLECTION';
45+
export const EDIT_COLLECTION = 'EDIT_COLLECTION';
46+
3947
export const DELETE_PROJECT = 'DELETE_PROJECT';
4048

4149
export const SET_SELECTED_FILE = 'SET_SELECTED_FILE';

client/images/check.svg

Lines changed: 6 additions & 0 deletions
Loading

client/images/check_encircled.svg

Lines changed: 11 additions & 0 deletions
Loading

client/images/close.svg

Lines changed: 12 additions & 0 deletions
Loading

client/modules/App/App.jsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class App extends React.Component {
1414

1515
componentDidMount() {
1616
this.setState({ isMounted: true }); // eslint-disable-line react/no-did-mount-set-state
17-
document.body.className = 'light';
17+
document.body.className = this.props.theme;
1818
}
1919

2020
componentWillReceiveProps(nextProps) {
@@ -26,6 +26,12 @@ class App extends React.Component {
2626
}
2727
}
2828

29+
componentDidUpdate(prevProps) {
30+
if (this.props.theme !== prevProps.theme) {
31+
document.body.className = this.props.theme;
32+
}
33+
}
34+
2935
render() {
3036
return (
3137
<div className="app">
@@ -45,10 +51,18 @@ App.propTypes = {
4551
}),
4652
}).isRequired,
4753
setPreviousPath: PropTypes.func.isRequired,
54+
theme: PropTypes.string,
4855
};
4956

5057
App.defaultProps = {
51-
children: null
58+
children: null,
59+
theme: 'light'
5260
};
5361

54-
export default connect(() => ({}), { setPreviousPath })(App);
62+
const mapStateToProps = state => ({
63+
theme: state.preferences.theme,
64+
});
65+
66+
const mapDispatchToProps = { setPreviousPath };
67+
68+
export default connect(mapStateToProps, mapDispatchToProps)(App);

client/modules/App/components/Overlay.jsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,12 @@ class Overlay extends React.Component {
6464
const {
6565
ariaLabel,
6666
title,
67-
children
67+
children,
68+
actions,
69+
isFixedHeight,
6870
} = this.props;
6971
return (
70-
<div className="overlay">
72+
<div className={`overlay ${isFixedHeight ? 'overlay--is-fixed-height' : ''}`}>
7173
<div className="overlay__content">
7274
<section
7375
role="main"
@@ -77,9 +79,12 @@ class Overlay extends React.Component {
7779
>
7880
<header className="overlay__header">
7981
<h2 className="overlay__title">{title}</h2>
80-
<button className="overlay__close-button" onClick={this.close} >
81-
<InlineSVG src={exitUrl} alt="close overlay" />
82-
</button>
82+
<div className="overlay__actions">
83+
{actions}
84+
<button className="overlay__close-button" onClick={this.close} >
85+
<InlineSVG src={exitUrl} alt="close overlay" />
86+
</button>
87+
</div>
8388
</header>
8489
{children}
8590
</section>
@@ -91,18 +96,22 @@ class Overlay extends React.Component {
9196

9297
Overlay.propTypes = {
9398
children: PropTypes.element,
99+
actions: PropTypes.element,
94100
closeOverlay: PropTypes.func,
95101
title: PropTypes.string,
96102
ariaLabel: PropTypes.string,
97-
previousPath: PropTypes.string
103+
previousPath: PropTypes.string,
104+
isFixedHeight: PropTypes.bool,
98105
};
99106

100107
Overlay.defaultProps = {
101108
children: null,
109+
actions: null,
102110
title: 'Modal',
103111
closeOverlay: null,
104112
ariaLabel: 'modal',
105-
previousPath: '/'
113+
previousPath: '/',
114+
isFixedHeight: false,
106115
};
107116

108117
export default Overlay;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import axios from 'axios';
2+
import * as ActionTypes from '../../../constants';
3+
import { startLoader, stopLoader } from './loader';
4+
import { setToastText, showToast } from './toast';
5+
6+
const __process = (typeof global !== 'undefined' ? global : window).process;
7+
const ROOT_URL = __process.env.API_URL;
8+
9+
const TOAST_DISPLAY_TIME_MS = 1500;
10+
11+
// eslint-disable-next-line
12+
export function getCollections(username) {
13+
return (dispatch) => {
14+
dispatch(startLoader());
15+
let url;
16+
if (username) {
17+
url = `${ROOT_URL}/${username}/collections`;
18+
} else {
19+
url = `${ROOT_URL}/collections`;
20+
}
21+
axios.get(url, { withCredentials: true })
22+
.then((response) => {
23+
dispatch({
24+
type: ActionTypes.SET_COLLECTIONS,
25+
collections: response.data
26+
});
27+
dispatch(stopLoader());
28+
})
29+
.catch((response) => {
30+
dispatch({
31+
type: ActionTypes.ERROR,
32+
error: response.data
33+
});
34+
dispatch(stopLoader());
35+
});
36+
};
37+
}
38+
39+
export function createCollection(collection) {
40+
return (dispatch) => {
41+
dispatch(startLoader());
42+
const url = `${ROOT_URL}/collections`;
43+
return axios.post(url, collection, { withCredentials: true })
44+
.then((response) => {
45+
dispatch({
46+
type: ActionTypes.CREATE_COLLECTION
47+
});
48+
dispatch(stopLoader());
49+
50+
const collectionName = response.data.name;
51+
dispatch(setToastText(`Created "${collectionName}"`));
52+
dispatch(showToast(TOAST_DISPLAY_TIME_MS));
53+
54+
return response.data;
55+
})
56+
.catch((response) => {
57+
dispatch({
58+
type: ActionTypes.ERROR,
59+
error: response.data
60+
});
61+
dispatch(stopLoader());
62+
63+
return response.data;
64+
});
65+
};
66+
}
67+
68+
export function addToCollection(collectionId, projectId) {
69+
return (dispatch) => {
70+
dispatch(startLoader());
71+
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`;
72+
return axios.post(url, { withCredentials: true })
73+
.then((response) => {
74+
dispatch({
75+
type: ActionTypes.ADD_TO_COLLECTION,
76+
payload: response.data
77+
});
78+
dispatch(stopLoader());
79+
80+
const collectionName = response.data.name;
81+
82+
dispatch(setToastText(`Added to "${collectionName}`));
83+
dispatch(showToast(TOAST_DISPLAY_TIME_MS));
84+
85+
return response.data;
86+
})
87+
.catch((response) => {
88+
dispatch({
89+
type: ActionTypes.ERROR,
90+
error: response.data
91+
});
92+
dispatch(stopLoader());
93+
94+
return response.data;
95+
});
96+
};
97+
}
98+
99+
export function removeFromCollection(collectionId, projectId) {
100+
return (dispatch) => {
101+
dispatch(startLoader());
102+
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`;
103+
return axios.delete(url, { withCredentials: true })
104+
.then((response) => {
105+
dispatch({
106+
type: ActionTypes.REMOVE_FROM_COLLECTION,
107+
payload: response.data
108+
});
109+
dispatch(stopLoader());
110+
111+
const collectionName = response.data.name;
112+
113+
dispatch(setToastText(`Removed from "${collectionName}`));
114+
dispatch(showToast(TOAST_DISPLAY_TIME_MS));
115+
116+
return response.data;
117+
})
118+
.catch((response) => {
119+
dispatch({
120+
type: ActionTypes.ERROR,
121+
error: response.data
122+
});
123+
dispatch(stopLoader());
124+
125+
return response.data;
126+
});
127+
};
128+
}
129+
130+
export function editCollection(collectionId, { name, description }) {
131+
return (dispatch) => {
132+
const url = `${ROOT_URL}/collections/${collectionId}`;
133+
return axios.patch(url, { name, description }, { withCredentials: true })
134+
.then((response) => {
135+
dispatch({
136+
type: ActionTypes.EDIT_COLLECTION,
137+
payload: response.data
138+
});
139+
return response.data;
140+
})
141+
.catch((response) => {
142+
dispatch({
143+
type: ActionTypes.ERROR,
144+
error: response.data
145+
});
146+
147+
return response.data;
148+
});
149+
};
150+
}
151+
152+
export function deleteCollection(collectionId) {
153+
return (dispatch) => {
154+
const url = `${ROOT_URL}/collections/${collectionId}`;
155+
return axios.delete(url, { withCredentials: true })
156+
.then((response) => {
157+
dispatch({
158+
type: ActionTypes.DELETE_COLLECTION,
159+
payload: response.data,
160+
collectionId,
161+
});
162+
return response.data;
163+
})
164+
.catch((response) => {
165+
dispatch({
166+
type: ActionTypes.ERROR,
167+
error: response.data
168+
});
169+
170+
return response.data;
171+
});
172+
};
173+
}

0 commit comments

Comments
 (0)