diff --git a/src/components/SelectMenu/component.js b/src/components/SelectMenu/component.js index 0c1cddd01..8c4656c31 100644 --- a/src/components/SelectMenu/component.js +++ b/src/components/SelectMenu/component.js @@ -1,10 +1,10 @@ -import React, { PureComponent } from 'react'; +import * as React from 'react'; import { TouchableOpacity } from 'react-native'; import PropTypes from 'prop-types'; import ActionSheet from 'react-native-actionsheet'; import * as colors from 'kitsu/constants/colors'; -export class SelectMenu extends PureComponent { +export class SelectMenu extends React.PureComponent { static propTypes = { cancelButtonIndex: PropTypes.number, children: PropTypes.element, @@ -26,18 +26,6 @@ export class SelectMenu extends PureComponent { tintColor: colors.black, }; - constructor(props) { - super(props); - - this.displayOptions = props.options.map((option) => { - if (typeof option === 'string') { - return option.charAt(0).toUpperCase() + option.slice(1); - } - - return option.text; - }); - } - getCancelButtonIndex() { return this.props.cancelButtonIndex > -1 ? this.props.cancelButtonIndex @@ -48,6 +36,14 @@ export class SelectMenu extends PureComponent { this.ActionSheet = component; }; + displayOptions = () => this.props.options.map((option) => { + if (typeof option === 'string') { + return option.charAt(0).toUpperCase() + option.slice(1); + } + + return option.text; + }); + handleFilterChange = (selectedIndex) => { const cancelButtonIndex = this.getCancelButtonIndex(); @@ -78,7 +74,7 @@ export class SelectMenu extends PureComponent { diff --git a/src/screens/Auth/LoginScreen.js b/src/screens/Auth/LoginScreen.js index 80670366b..43cddfa16 100644 --- a/src/screens/Auth/LoginScreen.js +++ b/src/screens/Auth/LoginScreen.js @@ -21,14 +21,13 @@ class LoginScreen extends Component { loading: false, }; - onSubmit = (isFb = false) => { + onSubmit = () => { const { username, password } = this.state; const { navigation } = this.props; - console.log('isFB', isFb, username, password); - if (isFb) { - this.props.loginUser(null, navigation); - } else if (username.length > 0 && password.length > 0) { + if (username.length > 0 && password.length > 0) { this.props.loginUser({ username, password }, navigation); + } else { + this.props.loginUser(null, navigation); } }; diff --git a/src/screens/Profiles/UserLibrary/components/UserLibraryListCard/component.js b/src/screens/Profiles/UserLibrary/components/UserLibraryListCard/component.js index e7076a8be..bdd9f6586 100644 --- a/src/screens/Profiles/UserLibrary/components/UserLibraryListCard/component.js +++ b/src/screens/Profiles/UserLibrary/components/UserLibraryListCard/component.js @@ -16,17 +16,25 @@ const USER_LIBRARY_EDIT_SCREEN = 'UserLibraryEdit'; export const STATUS_SELECT_OPTIONS = [ { value: 'current', anime: 'Currently Watching', manga: 'Currently Reading' }, { value: 'planned', anime: 'Want To Watch', manga: 'Want To Read' }, - { value: 'onHold', anime: 'On Hold', manga: 'On Hold' }, + { value: 'on_hold', anime: 'On Hold', manga: 'On Hold' }, { value: 'dropped', anime: 'Dropped', manga: 'Dropped' }, { value: 'completed', anime: 'Completed', manga: 'Completed' }, { value: 'remove', anime: 'Remove From Library', manga: 'Remove From Library' }, { value: 'cancel', anime: 'Nevermind', manga: 'Nevermind' }, ]; +const HEADER_TEXT_MAPPING = { + current: { anime: 'Watching', manga: 'Reading' }, + planned: { anime: 'Want To Watch', manga: 'Want to Read' }, + completed: { anime: 'Complete', manga: 'Complete' }, + on_hold: { anime: 'On Hold', manga: 'On Hold' }, + dropped: { anime: 'Dropped', manga: 'Dropped' }, +}; + export class UserLibraryListCard extends React.Component { static propTypes = { currentUser: PropTypes.object.isRequired, - data: PropTypes.object.isRequired, + libraryEntry: PropTypes.object.isRequired, libraryStatus: PropTypes.string.isRequired, libraryType: PropTypes.string.isRequired, navigate: PropTypes.func.isRequired, @@ -37,10 +45,12 @@ export class UserLibraryListCard extends React.Component { state = { isUpdating: false, - libraryStatus: this.props.data.status, - progress: this.props.data.progress, - progressPercentage: Math.floor((this.props.data.progress / this.getMaxProgress()) * 100), - ratingTwenty: this.props.data.ratingTwenty, + libraryStatus: this.props.libraryEntry.status, + progress: this.props.libraryEntry.progress, + progressPercentage: Math.floor( + (this.props.libraryEntry.progress / this.getMaxProgress()) * 100, + ), + ratingTwenty: this.props.libraryEntry.ratingTwenty, isSliderActive: false, } @@ -74,8 +84,8 @@ export class UserLibraryListCard extends React.Component { onSwipeRelease = () => this.props.onSwipingItem(false) getMaxProgress() { - const { data, libraryType } = this.props; - const mediaData = data[libraryType]; + const { libraryEntry, libraryType } = this.props; + const mediaData = libraryEntry[libraryType]; if (mediaData.type === 'anime') { return mediaData.episodeCount; @@ -111,11 +121,11 @@ export class UserLibraryListCard extends React.Component { // send the status from props because that is the list we're looking // at not the status from state because that is what the value of the // card may have just been changed to - const { libraryStatus, libraryType } = this.props; + const { libraryEntry, libraryType } = this.props; const { libraryStatus: newStatus, progress, ratingTwenty } = this.state; - await this.props.updateUserLibraryEntry(libraryType, libraryStatus, { - id: this.props.data.id, + this.props.updateUserLibraryEntry(libraryType, libraryEntry.status, { + id: this.props.libraryEntry.id, progress, ratingTwenty, status: newStatus, @@ -124,15 +134,15 @@ export class UserLibraryListCard extends React.Component { debounceSave = debounce(this.saveEntry, 200); - selectOptions = STATUS_SELECT_OPTIONS.map(option => ({ + selectOptions = () => STATUS_SELECT_OPTIONS.map(option => ({ value: option.value, text: option[this.props.libraryType], - })).filter(option => option.value !== this.props.libraryStatus); + })).filter(option => option.value !== this.props.libraryEntry.status); render() { - const { data, libraryType, currentUser } = this.props; + const { libraryEntry, libraryType, currentUser } = this.props; const { progressPercentage, isSliderActive } = this.state; - const mediaData = data[libraryType]; + const mediaData = libraryEntry[libraryType]; const canEdit = this.props.profile.id === this.props.currentUser.id; const maxProgress = this.getMaxProgress(); @@ -157,59 +167,72 @@ export class UserLibraryListCard extends React.Component { ]} > - - - - - - {mediaData.canonicalTitle} + { libraryEntry.status !== this.props.libraryStatus && + + + + Moved to + {HEADER_TEXT_MAPPING[libraryEntry.status][libraryType]} + - {canEdit && ( - - - - )} + - - - - - - + } + + + + + + + {mediaData.canonicalTitle} + + {canEdit && ( + + + + )} + + + + + + + + diff --git a/src/screens/Profiles/UserLibrary/components/UserLibraryListCard/styles.js b/src/screens/Profiles/UserLibrary/components/UserLibraryListCard/styles.js index f92a53bff..a4108d6c0 100644 --- a/src/screens/Profiles/UserLibrary/components/UserLibraryListCard/styles.js +++ b/src/screens/Profiles/UserLibrary/components/UserLibraryListCard/styles.js @@ -1,6 +1,6 @@ import { StyleSheet } from 'react-native'; import * as colors from 'kitsu/constants/colors'; -import { commonStyles } from 'kitsu/common/styles'; +import { commonStyles, flattenCommon } from 'kitsu/common/styles'; const MENU_BUTTON_WIDTH = 24; @@ -9,7 +9,6 @@ export const styles = StyleSheet.create({ backgroundColor: colors.white, borderBottomWidth: 1, borderColor: colors.lightGrey, - flexDirection: 'row', padding: 10, }, content: { @@ -19,12 +18,33 @@ export const styles = StyleSheet.create({ justifyContent: 'center', paddingLeft: 10, }, + horizontalRule: { + borderColor: colors.lightGrey, + flex: 1, + borderTopWidth: 1, + margin: 5, + }, menuButton: { width: MENU_BUTTON_WIDTH, }, menuButtonContainer: { marginLeft: 'auto', }, + metaDataContainer: { + flexDirection: 'row', + }, + moved: { + ...flattenCommon('centerCenter'), + flexDirection: 'row', + marginBottom: 10, + }, + movedText: { + color: colors.grey, + }, + movedToText: { + fontWeight: '600', + color: colors.grey, + }, posterImage: { borderRadius: 4, }, diff --git a/src/screens/Profiles/UserLibrary/components/UserLibraryListScreen/component.js b/src/screens/Profiles/UserLibrary/components/UserLibraryListScreen/component.js index 254009d24..89b664982 100644 --- a/src/screens/Profiles/UserLibrary/components/UserLibraryListScreen/component.js +++ b/src/screens/Profiles/UserLibrary/components/UserLibraryListScreen/component.js @@ -11,7 +11,7 @@ const HEADER_TEXT_MAPPING = { current: { anime: 'Watching', manga: 'Reading' }, planned: { anime: 'Want To Watch', manga: 'Want to Read' }, completed: { anime: 'Complete', manga: 'Complete' }, - onHold: { anime: 'On Hold', manga: 'On Hold' }, + on_hold: { anime: 'On Hold', manga: 'On Hold' }, dropped: { anime: 'Dropped', manga: 'Dropped' }, }; @@ -45,6 +45,7 @@ export class UserLibraryListScreenComponent extends React.Component { }; state = { + movedEntries: [], isSwiping: false, } @@ -61,6 +62,54 @@ export class UserLibraryListScreenComponent extends React.Component { }); }, 100); + // wrap the dispatch with a function that checks for "moved" entries (ie: current -> completed) + // when a library entry is moved, add it the the moved entries array in state. once we + // re-render, we'll splice these back into the rendered data array so that we can show the user + // that they were moved to a new section, we keep the list of moved entries in local state because + // they've been completely removed from their respective arrays in redux and we only want to show + // that something has been moved until the user navigates away + updateUserLibraryEntry = async (type, status, updates) => { + const { libraryEntries, libraryStatus } = this.props; + const { movedEntries } = this.state; + + let movedEntry; + let movedFromIndex; + const movedEntryIndex = movedEntries.findIndex(m => m.entry.id === updates.id); + // the first thing we want to check for is if the entry has already been moved. if it has, + // we want to remove it from the movedEntries array since properties are changing on it + if (movedEntryIndex > -1) { + movedEntry = movedEntries.splice(movedEntryIndex, 1)[0].entry; + movedFromIndex = movedEntry.index; + } + + // if the library entry has been updated to have a status that is not for the list we're looking + // at, it needs to get added to the moved entry list. + if (updates.status !== libraryStatus) { + // if we're not dealing with an entry that's already moved once, go find the entry in + // the current library entry list + if (!movedEntry) { + movedFromIndex = libraryEntries.data.findIndex( + libraryEntry => libraryEntry.id === updates.id, + ); + movedEntry = libraryEntries.data[movedFromIndex]; + } + + // finally push the moved entry onto the movedEntries array in state and override it with + // the updates + movedEntries.push({ + entry: { + ...movedEntry, + ...updates, + }, + index: movedFromIndex, + }); + } + + await this.props.updateUserLibraryEntry(type, status, updates); + + this.setState({ movedEntries }); + } + renderSearchBar = () => { const { profile } = this.props.navigation.state.params; @@ -76,25 +125,29 @@ export class UserLibraryListScreenComponent extends React.Component { renderItem = ({ item }) => ( ); render() { const { libraryEntries } = this.props; + const renderData = libraryEntries.data.slice(); + this.state.movedEntries.forEach(({ entry, index }) => { + renderData.splice(index, 0, entry); + }); return ( { + const { userLibrary } = this.props; + const { data, loading } = userLibrary[type][status]; + + if (loading && data.length) { + return ( + + + + ); + } + + return null; + } + renderLists = (type) => { const { userLibrary, navigation } = this.props; const listOrder = [ { status: 'current', anime: 'Watching', manga: 'Reading' }, { status: 'planned', anime: 'Want To Watch', manga: 'Want To Read' }, { status: 'completed', anime: 'Completed', manga: 'Completed' }, - { status: 'onHold', anime: 'On Hold', manga: 'On Hold' }, + { status: 'on_hold', anime: 'On Hold', manga: 'On Hold' }, { status: 'dropped', anime: 'Dropped', manga: 'Dropped' }, ]; @@ -206,6 +222,7 @@ export class UserLibraryScreenComponent extends React.Component { this.renderEmptyList(type, status) : async (dispatch, getState) const filter = { userId: options.userId, - status: options.status === 'onHold' ? 'on_hold' : options.status, + status: options.status, kind: options.library, }; @@ -221,16 +221,19 @@ export const fetchUserLibrary = fetchOptions => async (dispatch, getState) => { try { await Promise.all([ - fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'anime', status: 'completed' })(dispatch, getState), fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'anime', status: 'current' })(dispatch, getState), - fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'anime', status: 'dropped' })(dispatch, getState), - fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'anime', status: 'onHold' })(dispatch, getState), fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'anime', status: 'planned' })(dispatch, getState), - fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'manga', status: 'completed' })(dispatch, getState), + fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'anime', status: 'completed' })(dispatch, getState), + ]); + + await Promise.all([ + fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'anime', status: 'on_hold' })(dispatch, getState), + fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'anime', status: 'dropped' })(dispatch, getState), fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'manga', status: 'current' })(dispatch, getState), - fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'manga', status: 'dropped' })(dispatch, getState), - fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'manga', status: 'onHold' })(dispatch, getState), fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'manga', status: 'planned' })(dispatch, getState), + fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'manga', status: 'completed' })(dispatch, getState), + fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'manga', status: 'on_hold' })(dispatch, getState), + fetchUserLibraryByType({ ...fetchUserTypeOptions, library: 'manga', status: 'dropped' })(dispatch, getState), ]); dispatch({ @@ -307,17 +310,14 @@ export const updateUserLibraryEntry = ( try { const updateEntry = { ...newLibraryEntry }; - if (updateEntry.status === 'onHold') { - updateEntry.status = 'on_hold'; - } // optimistically update state dispatch({ libraryStatus, libraryType, - previousLibraryStatus: previousLibraryEntry.status === 'on_hold' ? 'onHold' : previousLibraryEntry.status, - newLibraryStatus: newLibraryEntry.status === 'on_hold' ? 'onHold' : newLibraryEntry.status, + previousLibraryStatus: previousLibraryEntry.status, + newLibraryStatus: newLibraryEntry.status, previousLibraryEntry, newLibraryEntry: updateEntry, diff --git a/src/store/profile/reducer.js b/src/store/profile/reducer.js index 50eae9463..e3adc03fa 100644 --- a/src/store/profile/reducer.js +++ b/src/store/profile/reducer.js @@ -23,14 +23,14 @@ const userLibraryInitial = { completed: { data: [], loading: false }, current: { data: [], loading: false }, dropped: { data: [], loading: false }, - onHold: { data: [], loading: false }, + on_hold: { data: [], loading: false }, planned: { data: [], loading: false }, }, manga: { completed: { data: [], loading: false }, current: { data: [], loading: false }, dropped: { data: [], loading: false }, - onHold: { data: [], loading: false }, + on_hold: { data: [], loading: false }, planned: { data: [], loading: false }, }, }; @@ -192,7 +192,7 @@ export const profileReducer = (state = INITIAL_STATE, action) => { ), }, - // add to newLibraryEntry.status (newLibraryStatus alias on_hold to onHold for us) + // add to newLibraryEntry.status [action.newLibraryStatus]: { ...state.userLibrary[action.libraryType][action.newLibraryStatus], data: [ @@ -310,7 +310,7 @@ export const profileReducer = (state = INITIAL_STATE, action) => { ), }, - // add to newLibraryEntry.status (newLibraryStatus alias on_hold to onHold for us) + // add to newLibraryEntry.status (newLibraryStatus alias on_hold to on_hold for us) [action.newLibraryStatus]: { ...state.userLibrarySearch[action.libraryType][action.newLibraryStatus], data: [