{
- this.props.sessionStatus === sessionActions.Status.FETCHED &&
- Object.keys(this.props.user).length !== 0 && // Only show if user is logged in
- Date.now() >= HOC_START_TIME && // Show middle banner on and after Dec 3
- Date.now() < HOC_END_TIME && // Hide middle banner after Dec 14
- false && // we did not use this middle banner in last HoC
-
-
-
+ this.props.shouldShowHOCMiddleBanner && (
+
+
+
+ )
}
= HOC_START_TIME &&
+ Date.now() < HOC_END_TIME
+ );
+ }
+ shouldShowHOCMiddleBanner () {
+ return false; // we did not use this middle banner in last HoC
+ }
+ shouldShowIntro () {
+ return (
+ this.props.sessionStatus === sessionActions.Status.FETCHED && // done fetching session
+ Object.keys(this.props.user).length === 0 && // no user session found
+ this.shouldShowHOCTopBanner() !== true
+ );
+ }
+ shouldShowDonateBanner () {
+ return (
+ this.state.dismissedDonateBanner === false &&
+ this.props.sessionStatus === sessionActions.Status.FETCHED && // done fetching session
+ Object.keys(this.props.user).length === 0 && // no user session found
+ Date.now() >= SCRATCH_WEEK_START_TIME &&
+ Date.now() < SCRATCH_WEEK_END_TIME &&
+ this.shouldShowHOCTopBanner() !== true
+ );
+ }
render () {
const showEmailConfirmation = this.shouldShowEmailConfirmation() || false;
+ const showDonateBanner = this.shouldShowDonateBanner() || false;
+ const showHOCTopBanner = this.shouldShowHOCTopBanner() || false;
+ const showHOCMiddleBanner = this.shouldShowHOCMiddleBanner() || false;
+ const showIntro = this.shouldShowIntro() || false;
const showWelcome = this.shouldShowWelcome();
const homepageRefreshStatus = this.getHomepageRefreshStatus();
@@ -163,9 +205,14 @@ class Splash extends React.Component {
refreshCacheStatus={homepageRefreshStatus}
sessionStatus={this.props.sessionStatus}
sharedByFollowing={this.props.shared}
+ shouldShowDonateBanner={showDonateBanner}
shouldShowEmailConfirmation={showEmailConfirmation}
+ shouldShowHOCTopBanner={showHOCTopBanner}
+ shouldShowIntro={showIntro}
+ shouldShowHOCMiddleBanner={showHOCMiddleBanner}
shouldShowWelcome={showWelcome}
user={this.props.user}
+ onCloseDonateBanner={this.handleCloseDonateBanner}
onCloseAdminPanel={this.handleCloseAdminPanel}
onDismiss={this.handleDismiss}
onHideEmailConfirmationModal={this.handleHideEmailConfirmationModal}
diff --git a/src/views/studio/l10n.json b/src/views/studio/l10n.json
new file mode 100644
index 000000000..c35e84f35
--- /dev/null
+++ b/src/views/studio/l10n.json
@@ -0,0 +1,48 @@
+{
+ "studio.tabNavProjects": "Projects",
+ "studio.tabNavCurators": "Curators",
+ "studio.tabNavComments": "Comments",
+ "studio.tabNavActivity": "Activity",
+
+ "studio.title": "Title",
+ "studio.description": "Description",
+ "studio.thumbnail": "Thumbnail",
+
+ "studio.projectsHeader": "Projects",
+ "studio.addProjectsHeader": "Add Projects",
+ "studio.addProject": "Add",
+
+ "studio.projectsEmptyCanAdd1": "Your studio is looking a little empty.",
+ "studio.projectsEmptyCanAdd2": "Add your first project!",
+ "studio.projectsEmpty1": "This studio has no projects yet.",
+ "studio.projectsEmpty2": "Suggest projects you want to add in the comments!",
+ "studio.browseProjects": "Browse Projects",
+
+ "studio.creatorRole": "Studio Creator",
+
+ "studio.managersHeader": "Managers",
+
+ "studio.unfollowStudio": "Unfollow Studio",
+ "studio.followStudio": "Follow Studio",
+
+ "studio.curatorsHeader": "Curators",
+ "studio.inviteCuratorsHeader": "Invite Curators",
+ "studio.inviteCurator": "Invite",
+ "studio.curatorAcceptInvite": "Accept Invite",
+ "studio.curatorsEmptyCanAdd1": "You don’t have curators right now.",
+ "studio.curatorsEmptyCanAdd2": "Add some curators to collaborate with!",
+ "studio.curatorsEmpty1": "This studio has no curators right now.",
+
+ "studio.commentsHeader": "Comments",
+
+ "studio.sharedFilter": "Shared",
+ "studio.favoritedFilter": "Favorited",
+ "studio.recentFilter": "Recent",
+
+ "studio.activityAddProjectToStudio": "{profileLink} added the project {projectLink}",
+ "studio.activityRemoveProjectStudio": "{profileLink} removed the project {projectLink}",
+ "studio.activityUpdateStudio": "{profileLink} made edits to the title, thumbnail, or description",
+ "studio.activityBecomeCurator": "{newCuratorProfileLink} accepted an invitation from {inviterProfileLink} to curate this studio",
+ "studio.activityRemoveCurator": "{removerProfileLink} removed the curator {removedProfileLink}",
+ "studio.activityBecomeOwner": "{promotedProfileLink} was promoted to manager by {promotorProfileLink}"
+}
diff --git a/src/views/studio/lib/fetchers.js b/src/views/studio/lib/fetchers.js
deleted file mode 100644
index 0b11e2e08..000000000
--- a/src/views/studio/lib/fetchers.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// TODO move this to studio-activity-actions, include pagination
-const activityFetcher = studioId =>
- fetch(`${process.env.API_HOST}/studios/${studioId}/activity`)
- .then(response => response.json())
- .then(data => ({items: data, moreToLoad: false})); // No pagination on the activity feed
-
-export {
- activityFetcher
-};
diff --git a/src/views/studio/lib/redux-modules.js b/src/views/studio/lib/redux-modules.js
index 05fb888d6..6cadf65d4 100644
--- a/src/views/studio/lib/redux-modules.js
+++ b/src/views/studio/lib/redux-modules.js
@@ -5,6 +5,8 @@ const curators = InfiniteList('curators');
const managers = InfiniteList('managers');
const activity = InfiniteList('activity');
+const userProjects = InfiniteList('user-projects');
+
export {
- projects, curators, managers, activity
+ projects, curators, managers, activity, userProjects
};
diff --git a/src/views/studio/lib/studio-activity-actions.js b/src/views/studio/lib/studio-activity-actions.js
new file mode 100644
index 000000000..4a694cfa3
--- /dev/null
+++ b/src/views/studio/lib/studio-activity-actions.js
@@ -0,0 +1,43 @@
+import keyMirror from 'keymirror';
+
+import api from '../../../lib/api';
+import {activity} from './redux-modules';
+import {selectStudioId} from '../../../redux/studio';
+
+const Errors = keyMirror({
+ NETWORK: null,
+ SERVER: null,
+ PERMISSION: null
+});
+
+const normalizeError = (err, body, res) => {
+ if (err) return Errors.NETWORK;
+ if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION;
+ if (res.statusCode !== 200) return Errors.SERVER;
+ return null;
+};
+
+const loadActivity = () => ((dispatch, getState) => {
+ const state = getState();
+ const studioId = selectStudioId(state);
+ const items = activity.selector(state).items;
+ const params = {limit: 20};
+ if (items.length > 0) {
+ // dateLimit is the newest notification you want to get back, which is
+ // the date of the oldest one we've already loaded
+ params.dateLimit = items[items.length - 1].datetime_created;
+ }
+ api({
+ uri: `/studios/${studioId}/activity/`,
+ params
+ }, (err, body, res) => {
+ const error = normalizeError(err, body, res);
+ if (error) return dispatch(activity.actions.error(error));
+ const ids = items.map(item => item.id);
+ // Deduplication is needed because pagination based on date can contain duplicates
+ const deduped = body.filter(item => ids.indexOf(item.id) === -1);
+ dispatch(activity.actions.append(deduped, body.length === params.limit));
+ });
+});
+
+export {loadActivity};
diff --git a/src/views/studio/lib/studio-member-actions.js b/src/views/studio/lib/studio-member-actions.js
index 2e8749b7b..894cb5354 100644
--- a/src/views/studio/lib/studio-member-actions.js
+++ b/src/views/studio/lib/studio-member-actions.js
@@ -8,13 +8,24 @@ import {selectStudioId, setRoles} from '../../../redux/studio';
const Errors = keyMirror({
NETWORK: null,
SERVER: null,
- PERMISSION: null
+ PERMISSION: null,
+ DUPLICATE: null,
+ UNKNOWN_USERNAME: null,
+ RATE_LIMIT: null
});
const normalizeError = (err, body, res) => {
if (err) return Errors.NETWORK;
if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION;
+ if (res.statusCode === 404) return Errors.UNKNOWN_USERNAME;
+ if (res.statusCode === 429) return Errors.RATE_LIMIT;
if (res.statusCode !== 200) return Errors.SERVER;
+ if (body && body.status === 'error') {
+ if (body.message.indexOf('already a curator') !== -1) {
+ return Errors.DUPLICATE;
+ }
+ return Errors.UNHANDLED;
+ }
return null;
};
@@ -132,7 +143,7 @@ const promoteCurator = username => ((dispatch, getState) => new Promise((resolve
const index = curatorList.findIndex(v => v.username === username);
const curatorItem = curatorList[index];
if (index !== -1) dispatch(curators.actions.remove(index));
- dispatch(managers.actions.create(curatorItem));
+ dispatch(managers.actions.create(curatorItem, true));
return resolve();
});
}));
@@ -156,7 +167,7 @@ const acceptInvitation = () => ((dispatch, getState) => new Promise((resolve, re
if (userError) return reject(userError);
// Note: this assumes that the user items from the curator endpoint
// are the same structure as the single user data returned from /users/:username
- dispatch(curators.actions.create(userBody));
+ dispatch(curators.actions.create(userBody, true));
dispatch(setRoles({invited: false, curator: true}));
return resolve();
});
diff --git a/src/views/studio/lib/studio-project-actions.js b/src/views/studio/lib/studio-project-actions.js
index 0de1015df..a1b1c9c27 100644
--- a/src/views/studio/lib/studio-project-actions.js
+++ b/src/views/studio/lib/studio-project-actions.js
@@ -9,12 +9,16 @@ import {projects} from './redux-modules';
const Errors = keyMirror({
NETWORK: null,
SERVER: null,
- PERMISSION: null
+ PERMISSION: null,
+ UNKNOWN_PROJECT: null,
+ RATE_LIMIT: null
});
const normalizeError = (err, body, res) => {
if (err) return Errors.NETWORK;
if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION;
+ if (res.statusCode === 404) return Errors.UNKNOWN_PROJECT;
+ if (res.statusCode === 429) return Errors.RATE_LIMIT;
if (res.statusCode !== 200) return Errors.SERVER;
return null;
};
@@ -46,7 +50,7 @@ const loadProjects = () => ((dispatch, getState) => {
*/
const generateProjectListItem = (postBody, infoBody) => ({
// Fields from the POST to add the project to the studio
- id: postBody.projectId,
+ id: parseInt(postBody.projectId, 10),
actor_id: postBody.actorId,
// Fields from followup GET for more project info
title: infoBody.title,
diff --git a/src/views/studio/lib/user-projects-actions.js b/src/views/studio/lib/user-projects-actions.js
new file mode 100644
index 000000000..b932ef206
--- /dev/null
+++ b/src/views/studio/lib/user-projects-actions.js
@@ -0,0 +1,59 @@
+import keyMirror from 'keymirror';
+import api from '../../../lib/api';
+import {selectUsername} from '../../../redux/session';
+import {userProjects, projects} from './redux-modules';
+
+const Errors = keyMirror({
+ NETWORK: null,
+ SERVER: null,
+ PERMISSION: null
+});
+
+const Filters = keyMirror({
+ SHARED: null,
+ FAVORITED: null,
+ RECENT: null
+});
+
+const Uris = {
+ [Filters.SHARED]: username => `/users/${username}/projects`,
+ [Filters.FAVORITED]: username => `/users/${username}/favorites`,
+ [Filters.RECENT]: username => `/users/${username}/recent`
+};
+
+const normalizeError = (err, body, res) => {
+ if (err) return Errors.NETWORK;
+ if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION;
+ if (res.statusCode !== 200) return Errors.SERVER;
+ return null;
+};
+
+const loadUserProjects = type => ((dispatch, getState) => {
+ const state = getState();
+ const username = selectUsername(state);
+ const projectCount = userProjects.selector(state).items.length;
+ const projectsPerPage = 20;
+ dispatch(userProjects.actions.loading());
+ api({
+ uri: Uris[type](username),
+ params: {limit: projectsPerPage, offset: projectCount}
+ }, (err, body, res) => {
+ const error = normalizeError(err, body, res);
+ if (error) return dispatch(userProjects.actions.error(error));
+ const moreToLoad = body.length === projectsPerPage;
+ const studioProjectIds = projects.selector(getState()).items.map(item => item.id);
+ const loadedProjects = body.map(item => Object.assign(item, {
+ inStudio: studioProjectIds.indexOf(item.id) !== -1
+ }));
+ dispatch(userProjects.actions.append(loadedProjects, moreToLoad));
+ });
+});
+
+// Re-export clear so that the consumer can manage filter changes
+const clearUserProjects = userProjects.actions.clear;
+
+export {
+ Filters,
+ loadUserProjects,
+ clearUserProjects
+};
diff --git a/src/views/studio/modals/user-projects-modal.jsx b/src/views/studio/modals/user-projects-modal.jsx
new file mode 100644
index 000000000..cf6c57051
--- /dev/null
+++ b/src/views/studio/modals/user-projects-modal.jsx
@@ -0,0 +1,121 @@
+/* eslint-disable react/jsx-no-bind */
+import React, {useEffect, useState} from 'react';
+import PropTypes from 'prop-types';
+import {connect} from 'react-redux';
+import classNames from 'classnames';
+import {FormattedMessage} from 'react-intl';
+
+import {addProject, removeProject} from '../lib/studio-project-actions';
+import {userProjects} from '../lib/redux-modules';
+import {Filters, loadUserProjects, clearUserProjects} from '../lib/user-projects-actions';
+
+import Modal from '../../../components/modal/base/modal.jsx';
+import ModalTitle from '../../../components/modal/base/modal-title.jsx';
+import ModalInnerContent from '../../../components/modal/base/modal-inner-content.jsx';
+import SubNavigation from '../../../components/subnavigation/subnavigation.jsx';
+import UserProjectsTile from './user-projects-tile.jsx';
+
+import './user-projects-modal.scss';
+
+const UserProjectsModal = ({
+ items, error, loading, moreToLoad, onLoadMore, onClear,
+ onAdd, onRemove, onRequestClose
+}) => {
+ const [filter, setFilter] = useState(Filters.SHARED);
+
+ useEffect(() => {
+ onClear();
+ onLoadMore(filter);
+ }, [filter]);
+
+ return (
+
+
+
+
setFilter(Filters.SHARED)}
+ >
+
+
+
setFilter(Filters.FAVORITED)}
+ >
+
+
+
setFilter(Filters.RECENT)}
+ >
+
+
+
+
+ {error &&
Error loading {filter}: {error}
}
+
+ {items.map(project => (
+
+ ))}
+
+ {loading ? Loading... : (
+ moreToLoad ?
+ :
+ No more to load
+ )}
+