From 97939d528e675dc98faa9e8c213124166d5c2983 Mon Sep 17 00:00:00 2001 From: Mark Bouslog Date: Thu, 29 Nov 2018 01:12:26 -0600 Subject: [PATCH] Subject card fav button (#24) * Remove unneeded default project context value * Add favorites context * Refactor favorites container * Init fav functionality * Refactor to favoriteCollection and linkedSubjects --- src/components/App.jsx | 83 ++++++++------- src/components/FavoritesButton.jsx | 48 +++++++++ src/components/SubjectCard.jsx | 19 ++-- src/containers/FavoritesContainer.jsx | 68 +++++------- src/context/FavoritesContext.jsx | 147 ++++++++++++++++++++++++++ src/context/ProjectContext.jsx | 4 +- 6 files changed, 284 insertions(+), 85 deletions(-) create mode 100644 src/components/FavoritesButton.jsx create mode 100644 src/context/FavoritesContext.jsx diff --git a/src/components/App.jsx b/src/components/App.jsx index dc940f0..b4c4fcf 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -3,8 +3,13 @@ import GrommetApp from 'grommet/components/App'; import Box from 'grommet/components/Box'; import { ZooFooter, ZooHeader } from 'zooniverse-react-components'; +import { + FavoritesProvider, + FavoritesContext +} from '../context/FavoritesContext'; import { ProjectProvider, ProjectContext } from '../context/ProjectContext'; import { UserProvider, UserContext } from '../context/UserContext'; + import AuthContainer from '../containers/AuthContainer'; import UserHeading from './UserHeading'; import RecentsContainer from '../containers/RecentsContainer'; @@ -17,45 +22,51 @@ const App = () => ( - {userContext => ( + {({ user }) => ( } /> - - - {projectContext => ( - - )} - - - - -
- + + {({ project }) => ( + + + + + + {({ favoriteCollection, linkedSubjects }) => ( + + +
+ +
+ )} +
+
+ +
+ + Your Badges +
- -
- - Your Badges - -
+ )} +
)} diff --git a/src/components/FavoritesButton.jsx b/src/components/FavoritesButton.jsx new file mode 100644 index 0000000..2405dc1 --- /dev/null +++ b/src/components/FavoritesButton.jsx @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Button from 'grommet/components/Button'; + +export default function FavoritesButton({ + addSubjectTo, + linkedSubjects, + removeSubjectFrom, + subject +}) { + const subjectId = subject.links.subject || subject.id; + const favorited = + linkedSubjects && linkedSubjects.length + ? !!linkedSubjects.some(subj => subj === subjectId) + : false; + + const favClassName = favorited ? 'fas fa-heart fa-fw' : 'far fa-heart fa-fw'; + + return ( + + ); +} + +FavoritesButton.defaultProps = { + addSubjectTo: () => {}, + linkedSubjects: [], + removeSubjectFrom: () => {} +}; + +FavoritesButton.propTypes = { + addSubjectTo: PropTypes.func, + linkedSubjects: PropTypes.arrayOf(PropTypes.string), + removeSubjectFrom: PropTypes.func, + subject: PropTypes.shape({ + id: PropTypes.string + }).isRequired +}; diff --git a/src/components/SubjectCard.jsx b/src/components/SubjectCard.jsx index dc1e497..19de1c6 100644 --- a/src/components/SubjectCard.jsx +++ b/src/components/SubjectCard.jsx @@ -1,12 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; import Box from 'grommet/components/Box'; -import Button from 'grommet/components/Button'; import { Thumbnail } from 'zooniverse-react-components'; import { config } from '../config'; import { ProjectContext } from '../context/ProjectContext'; import getSubjectLocations from '../lib/get-subject-locations'; +import { FavoritesContext } from '../context/FavoritesContext'; +import FavoritesButton from './FavoritesButton'; export default function SubjectCard({ subject }) { const locations = getSubjectLocations(subject); @@ -45,12 +46,16 @@ export default function SubjectCard({ subject }) { /> - - + + {({ addSubjectTo, linkedSubjects, removeSubjectFrom }) => ( + + )} + )} diff --git a/src/containers/FavoritesContainer.jsx b/src/containers/FavoritesContainer.jsx index 723d6d6..bc616a7 100644 --- a/src/containers/FavoritesContainer.jsx +++ b/src/containers/FavoritesContainer.jsx @@ -1,10 +1,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import Box from 'grommet/components/Box'; -import apiClient from 'panoptes-client'; import { Paginator } from 'zooniverse-react-components'; -import { config } from '../config'; import Title from '../components/Title'; import SubjectCard from '../components/SubjectCard'; @@ -12,7 +10,7 @@ class FavoritesContainer extends React.Component { constructor() { super(); this.state = { - favorites: null, + favoriteSubjects: null, meta: null }; @@ -20,56 +18,46 @@ class FavoritesContainer extends React.Component { } componentDidMount() { - this.fetchFavorites(); + this.fetchFavoriteSubjects(); } componentDidUpdate(prevProps) { - if (prevProps.user !== this.props.user) { - this.fetchFavorites(); + if (prevProps.favoriteCollection !== this.props.favoriteCollection) { + this.fetchFavoriteSubjects(); } } onPageChange(page) { - this.fetchFavorites(page); + this.fetchFavoriteSubjects(page); } - fetchFavorites(page = 1) { - const { user } = this.props; + fetchFavoriteSubjects(page = 1) { + const { favoriteCollection, linkedSubjects } = this.props; - if (user) { + if (favoriteCollection && linkedSubjects.length) { const query = { page, page_size: 3 }; - apiClient - .type('collections') - .get({ - project_ids: config.projectId, - favorite: true, - sort: 'display_name' - }) - .then(collections => { - if (collections && collections[0]) { - collections[0] - .get('subjects', query) - .then(favorites => - this.setState({ favorites, meta: favorites[0].getMeta() }) - ) - .catch(() => { - if (console) { - console.warn('Failed to fetch favorites'); - } - }); - } - }) + favoriteCollection + .get('subjects', query) + .then(favoriteSubjects => + this.setState({ + favoriteSubjects, + meta: favoriteSubjects[0].getMeta() + }) + ) .catch(() => { if (console) { - console.warn('Failed to fetch colletions for favorites'); + console.warn('Failed to fetch favorites'); } }); } else { - this.setState({ meta: null, favorites: null }); + this.setState({ + favoriteSubjects: null, + meta: null + }); } } @@ -78,8 +66,8 @@ class FavoritesContainer extends React.Component { Your Favorites - {this.state.favorites && - this.state.favorites.map(favorite => ( + {this.state.favoriteSubjects && + this.state.favoriteSubjects.map(favorite => ( ))} @@ -97,13 +85,15 @@ class FavoritesContainer extends React.Component { } FavoritesContainer.propTypes = { - user: PropTypes.shape({ - get: PropTypes.func - }) + favoriteCollection: PropTypes.shape({ + id: PropTypes.string + }), + linkedSubjects: PropTypes.arrayOf(PropTypes.string) }; FavoritesContainer.defaultProps = { - user: null + favoriteCollection: null, + linkedSubjects: [] }; export default FavoritesContainer; diff --git a/src/context/FavoritesContext.jsx b/src/context/FavoritesContext.jsx new file mode 100644 index 0000000..978d76a --- /dev/null +++ b/src/context/FavoritesContext.jsx @@ -0,0 +1,147 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import apiClient from 'panoptes-client/lib/api-client'; + +export const FavoritesContext = React.createContext(); + +export class FavoritesProvider extends Component { + constructor(props) { + super(props); + this.state = { + favoriteCollection: null, + linkedSubjects: [], + initialised: false + }; + + this.addSubjectTo = this.addSubjectTo.bind(this); + this.removeSubjectFrom = this.removeSubjectFrom.bind(this); + } + + componentDidMount() { + const { project, user } = this.props; + const { initialised } = this.state; + if (!initialised && project && user) { + this.fetchFavoriteCollection(); + } + } + + componentDidUpdate(prevProps) { + const { project, user } = this.props; + if (prevProps.project !== project || prevProps.user !== user) { + if (project && user) { + this.fetchFavoriteCollection(); + } + } + } + + fetchFavoriteCollection() { + const { project, user } = this.props; + apiClient + .type('collections') + .get({ + owner: user.login, + project_ids: project.id, + favorite: true + }) + .then(([collections]) => { + if (collections) { + this.setState({ + initialised: true, + favoriteCollection: collections, + linkedSubjects: + collections.links && collections.links.subjects + ? collections.links.subjects + : [] + }); + } + }); + } + + addSubjectTo(subjectId) { + const { favoriteCollection, linkedSubjects } = this.state; + if (favoriteCollection) { + favoriteCollection + .addLink('subjects', [subjectId.toString()]) + .then(() => { + this.setState({ + linkedSubjects: [...linkedSubjects, subjectId.toString()] + }); + }) + .catch(() => console.warn('Failed to save favorite.')); + } else { + this.createFavorites([subjectId]); + } + } + + removeSubjectFrom(subjectId) { + const { favoriteCollection, linkedSubjects } = this.state; + favoriteCollection + .removeLink('subjects', [subjectId.toString()]) + .then(() => { + this.setState({ + linkedSubjects: linkedSubjects.filter( + subject => subject !== subjectId.toString() + ) + }); + }) + .catch(() => console.warn('Failed to remove favorite.')); + } + + createFavorites(subjects = []) { + return new Promise((resolve, reject) => { + const display_name = `Favorites ${this.props.project.slug}`; + const project = this.props.project.id; + const favorite = true; + + const links = { project, subjects }; + const collection = { favorite, display_name, links }; + apiClient + .type('collections') + .create(collection) + .save() + .catch(err => { + reject(err); + }) + .then(([newFavoritesCollection]) => { + this.setState({ + favoriteCollection: newFavoritesCollection, + linkedSubjects: subjects + }); + resolve(); + }); + }); + } + + render() { + const { favoriteCollection, linkedSubjects, initialised } = this.state; + return ( + + {this.props.children} + + ); + } +} + +FavoritesProvider.propTypes = { + children: PropTypes.element.isRequired, + project: PropTypes.shape({ + id: PropTypes.string, + slug: PropTypes.string + }), + user: PropTypes.shape({ + login: PropTypes.string + }) +}; + +FavoritesProvider.defaultProps = { + project: null, + user: null +}; diff --git a/src/context/ProjectContext.jsx b/src/context/ProjectContext.jsx index abd08a3..09505a5 100644 --- a/src/context/ProjectContext.jsx +++ b/src/context/ProjectContext.jsx @@ -4,9 +4,7 @@ import apiClient from 'panoptes-client/lib/api-client'; import { config } from '../config'; -export const ProjectContext = React.createContext({ - id: config.projectId -}); +export const ProjectContext = React.createContext(); export class ProjectProvider extends Component { constructor(props) {