diff --git a/android/app/build.gradle b/android/app/build.gradle index 1149067..7435a73 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -143,8 +143,8 @@ android { applicationId "com.kyonru.justareviewapp" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 4 - versionName "1.1.0" + versionCode 5 + versionName "1.2.0" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' multiDexEnabled true diff --git a/fastlane/report.xml b/fastlane/report.xml index 3e43cbf..cd2ab8d 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,42 +5,42 @@ - + - + - + - + - + - + - + - + diff --git a/index.js b/index.js index 62120ce..9fea50d 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ /** * @format */ - +import 'react-native-gesture-handler'; import { AppRegistry } from 'react-native'; import App from 'src'; import { name as appName } from './app.json'; diff --git a/ios/justAReviewApp-tvOS/Info.plist b/ios/justAReviewApp-tvOS/Info.plist index d5fb005..c56e316 100644 --- a/ios/justAReviewApp-tvOS/Info.plist +++ b/ios/justAReviewApp-tvOS/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.0 + 1.2.0 CFBundleSignature ???? CFBundleVersion - 4 + 5 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/ios/justAReviewApp-tvOSTests/Info.plist b/ios/justAReviewApp-tvOSTests/Info.plist index 34a3f0b..9724753 100644 --- a/ios/justAReviewApp-tvOSTests/Info.plist +++ b/ios/justAReviewApp-tvOSTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.1.0 + 1.2.0 CFBundleSignature ???? CFBundleVersion - 4 + 5 diff --git a/ios/justAReviewApp.xcodeproj/project.pbxproj b/ios/justAReviewApp.xcodeproj/project.pbxproj index 88607c7..02bb5e1 100644 --- a/ios/justAReviewApp.xcodeproj/project.pbxproj +++ b/ios/justAReviewApp.xcodeproj/project.pbxproj @@ -774,7 +774,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = justAReviewApp/justAReviewApp.entitlements; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = YES; INFOPLIST_FILE = justAReviewApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -805,7 +805,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = justAReviewApp/justAReviewApp.entitlements; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; INFOPLIST_FILE = justAReviewApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( diff --git a/ios/justAReviewApp/Info.plist b/ios/justAReviewApp/Info.plist index 7e394de..9fb74d2 100644 --- a/ios/justAReviewApp/Info.plist +++ b/ios/justAReviewApp/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.0 + 1.2.0 CFBundleSignature ???? CFBundleURLTypes @@ -34,7 +34,7 @@ CFBundleVersion - 4 + 5 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/ios/justAReviewAppTests/Info.plist b/ios/justAReviewAppTests/Info.plist index 5a315c7..0a25e7d 100644 --- a/ios/justAReviewAppTests/Info.plist +++ b/ios/justAReviewAppTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.1.0 + 1.2.0 CFBundleSignature ???? CFBundleVersion - 4 + 5 diff --git a/package.json b/package.json index 7dbfff5..3685d32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "justAReviewApp", - "version": "1.1.0", + "version": "1.2.0", "private": true, "scripts": { "android": "react-native run-android", @@ -57,6 +57,7 @@ "react-native-paper": "^4.3.1", "react-native-push-notification": "^6.1.3", "react-native-reanimated": "^1.13.1", + "react-native-redash": "^16.0.8", "react-native-safe-area-context": "^3.1.8", "react-native-screens": "^2.13.0", "react-native-svg": "^12.1.0", diff --git a/src/@types/external.modules.d.ts b/src/@types/external.modules.d.ts index 4eeab7d..119f698 100644 --- a/src/@types/external.modules.d.ts +++ b/src/@types/external.modules.d.ts @@ -9,4 +9,6 @@ declare module '@env' { export const PAY_URL: string; export const GITHUB_URL: string; export const SENTRY: string; + export const TRANSLATIONS_URL: string; + export const BUG_REPORTING_URL: string; } diff --git a/src/@types/services.ts b/src/@types/services.ts index 7797d5f..a7672cb 100644 --- a/src/@types/services.ts +++ b/src/@types/services.ts @@ -108,4 +108,25 @@ export enum LanguageSource { saturday = 'saturday', sunday = 'sunday', item = 'item', + showOnboarding = 'showOnboarding', + onboardingTitle1 = 'onboardingTitle1', + onboardingTitle2 = 'onboardingTitle2', + onboardingTitle3 = 'onboardingTitle3', + onboardingTitle4 = 'onboardingTitle4', + onboardingTitle5 = 'onboardingTitle5', + onboardingDescription1 = 'onboardingDescription1', + onboardingDescription2 = 'onboardingDescription2', + onboardingDescription3 = 'onboardingDescription3', + onboardingDescription4 = 'onboardingDescription4', + onboardingDescription5 = 'onboardingDescription5', + next = 'next', + close = 'close', + skip = 'skip', + helpTranslate = 'helpTranslate', + reportBug = 'reportBug', + nextReminder = 'nextReminder', + skipToNextDate = 'skipToNextDate', + seeLogs = 'seeLogs', + showHidden = 'showHidden', + hideHidden = 'hideHidden', } diff --git a/src/components/screen-container/index.tsx b/src/components/screen-container/index.tsx index 36d0996..9094242 100644 --- a/src/components/screen-container/index.tsx +++ b/src/components/screen-container/index.tsx @@ -15,7 +15,7 @@ function ScreenContainer(props: Props) { const { containerStyle, children, containerProps } = props; return ( <> - + { + return ( + + + + + {title} + + + + ); +}; + +const OnBoarding = ({ show, onDismiss }: OnBoardingProps) => { + const [currentStep, setCurrentStep] = useState(0); + const x = useValue(0); + const onScroll = (event: NativeSyntheticEvent) => { + const offset = event.nativeEvent.contentOffset.x as any; + x.setValue(offset); + const step = Math.ceil(+offset / width); + + if (currentStep !== step) { + setCurrentStep(step); + } + }; + const scrollView = React.createRef(); + + const onNext = () => { + if (currentStep === steps.length - 1) { + onDismiss(); + setCurrentStep(0); + x.setValue(0); + return; + } + const nextStep = currentStep + 1; + scrollView.current + ?.getNode() + ?.scrollTo({ x: nextStep * width, animated: true }); + }; + + const backgroundColor = interpolateColors(x, { + inputRange: [0, width, width * 2, width * 3, width * 4], + outputColorRange: ['#00B8A9', '#3F72AF', '#023436', '#64403E', '#815355'], + }) as any; + + const { translate, strings } = React.useContext(LocalizationContext); + + return ( + + + + + + {steps.map((step, index) => ( + 0} + /> + ))} + + + + + + {translate(steps[currentStep].description)} + + + + + {currentStep === steps.length - 1 + ? translate(strings.close) + : translate(strings.next)} + + + + {currentStep === steps.length - 1 ? null : ( + + {`${translate( + strings.skip, + )} >`} + + )} + + + + + + ); +}; + +interface OnBoardingProps { + show: boolean; + onDismiss(): void; +} + +export default OnBoarding; diff --git a/src/containers/onboarding/styles.ts b/src/containers/onboarding/styles.ts new file mode 100644 index 0000000..6192439 --- /dev/null +++ b/src/containers/onboarding/styles.ts @@ -0,0 +1,76 @@ +import { + StyleSheet, + Dimensions, + ViewStyle, + TextStyle, + ImageStyle, +} from 'react-native'; + +const { width, height } = Dimensions.get('window'); + +interface OnboardingStyles { + container: ViewStyle; + slideText: TextStyle; + descriptionText: TextStyle; + descriptionSlide: ViewStyle; + headerText: TextStyle; + right: ViewStyle; + left: ViewStyle; + bottom: ViewStyle; + descriptionContainer: ViewStyle; + top: ViewStyle; + image: ImageStyle; + skipButton: ViewStyle; + skipText: TextStyle; +} + +export default StyleSheet.create({ + container: { + width, + height, + backgroundColor: 'white', + }, + slideText: { + color: 'white', + fontSize: 16, + }, + descriptionText: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 20, + }, + descriptionSlide: { + color: '#1F2421', + paddingTop: 20, + }, + headerText: { + fontSize: 40, + lineHeight: 46, + fontWeight: 'bold', + }, + right: { right: -height / 8 - 60 }, + left: { left: -height / 8 - 60 }, + bottom: { + backgroundColor: 'white', + justifyContent: 'space-between', + alignItems: 'center', + borderTopLeftRadius: 40, + padding: 40, + flex: 1, + }, + descriptionContainer: { + height: 42, + width: 120, + justifyContent: 'center', + alignItems: 'center', + borderRadius: 40, + }, + top: { + paddingTop: 20, + flex: 0.6, + borderBottomRightRadius: 40, + }, + image: { height: 250, width: 250 }, + skipButton: { position: 'absolute', right: 40, bottom: 50 }, + skipText: { color: '#728379', height: 20 }, +}); diff --git a/src/resources/images/onboarding/1.png b/src/resources/images/onboarding/1.png new file mode 100644 index 0000000..62f6481 Binary files /dev/null and b/src/resources/images/onboarding/1.png differ diff --git a/src/resources/images/onboarding/2.png b/src/resources/images/onboarding/2.png new file mode 100644 index 0000000..9892467 Binary files /dev/null and b/src/resources/images/onboarding/2.png differ diff --git a/src/resources/images/onboarding/3.png b/src/resources/images/onboarding/3.png new file mode 100644 index 0000000..7ad769d Binary files /dev/null and b/src/resources/images/onboarding/3.png differ diff --git a/src/resources/images/onboarding/4.png b/src/resources/images/onboarding/4.png new file mode 100644 index 0000000..92edf02 Binary files /dev/null and b/src/resources/images/onboarding/4.png differ diff --git a/src/resources/images/onboarding/5.png b/src/resources/images/onboarding/5.png new file mode 100644 index 0000000..d9e2e38 Binary files /dev/null and b/src/resources/images/onboarding/5.png differ diff --git a/src/resources/index.ts b/src/resources/index.ts index d8e2075..e251ff5 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -14,5 +14,12 @@ export default { design3: require('./images/empty-states/design-3.png'), coding: require('./images/empty-states/coding.png'), }, + onBoarding: { + illustration1: require('./images/onboarding/1.png'), + illustration2: require('./images/onboarding/2.png'), + illustration3: require('./images/onboarding/3.png'), + illustration4: require('./images/onboarding/4.png'), + illustration5: require('./images/onboarding/5.png'), + }, }, }; diff --git a/src/screens/drawer/about/index.tsx b/src/screens/drawer/about/index.tsx index 916fcb9..153a66c 100644 --- a/src/screens/drawer/about/index.tsx +++ b/src/screens/drawer/about/index.tsx @@ -2,7 +2,14 @@ import React, { useState, useEffect } from 'react'; import { Image, View, Linking } from 'react-native'; import { List, Subheading, DataTable } from 'react-native-paper'; import { ScrollView } from 'react-native-gesture-handler'; -import { ART_URL, CHANNEL_URL, PAY_URL, GITHUB_URL } from '@env'; +import { + ART_URL, + CHANNEL_URL, + PAY_URL, + GITHUB_URL, + TRANSLATIONS_URL, + BUG_REPORTING_URL, +} from '@env'; import ScreenContainer from 'src/components/screen-container'; import app from 'src/data/app'; @@ -85,6 +92,18 @@ function AboutApp(props: any) { title={translate(strings.invitePizza)} rippleColor={`${colors.shamrock}22`} /> + Linking.openURL(TRANSLATIONS_URL) as any} + style={styles.item} + title={translate(strings.helpTranslate)} + rippleColor={`${colors.shamrock}22`} + /> + Linking.openURL(BUG_REPORTING_URL) as any} + style={styles.item} + title={translate(strings.reportBug)} + rippleColor={`${colors.shamrock}22`} + /> diff --git a/src/screens/drawer/about/styles.ts b/src/screens/drawer/about/styles.ts index c2ea50f..0bd2e9d 100644 --- a/src/screens/drawer/about/styles.ts +++ b/src/screens/drawer/about/styles.ts @@ -31,7 +31,13 @@ export default StyleSheet.create<{ borderBottomColor: `${colors.lynch}33`, borderBottomWidth: 1, }, - bottomImage: { width: 300, height: 300, alignSelf: 'center' }, + bottomImage: { + width: 300, + height: 300, + alignSelf: 'center', + zIndex: -99, + opacity: 0.8, + }, message: { textAlign: 'center', padding: 8, fontSize: 12 }, kyonru: { color: colors.lynch, fontSize: 12 }, love: { color: 'red', fontSize: 12 }, diff --git a/src/screens/main/index.tsx b/src/screens/main/index.tsx index 84a68d1..f4f11bc 100644 --- a/src/screens/main/index.tsx +++ b/src/screens/main/index.tsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import ScreenContainer from 'src/components/screen-container'; import FABButton from 'src/components/button/fab'; +import OnBoarding from 'src/containers/onboarding'; import SectionReviewList from 'src/containers/review-list/section'; import colors from 'src/theme/colors'; @@ -60,7 +61,13 @@ class MainScreen extends Component< render() { const { showFAB } = this.state; - const { navigation, reviews, archiveCount } = this.props; + const { + navigation, + reviews, + archiveCount, + showOnboarding, + hideOnboarding, + } = this.props; const { translate, strings } = this.context; return ( + ); } diff --git a/src/screens/main/props.ts b/src/screens/main/props.ts index 8952aaf..fddec46 100644 --- a/src/screens/main/props.ts +++ b/src/screens/main/props.ts @@ -7,12 +7,16 @@ import { } from 'src/store/reviews/selector'; import { Review } from 'src/@types'; import { Store } from 'src/@types/store'; +import settingsSlice from 'src/store/settings/reducer'; +import { getShowOnBoarding } from 'src/store/settings/selectors'; export interface ReviewsMainScreenAppProps { navigation: any; addReview: (review: Review) => Promise; reviews: SectionListData[]; archiveCount: number; + showOnboarding: boolean; + hideOnboarding(): void; } export interface ReviewsMainScreenAppState { @@ -22,8 +26,11 @@ export interface ReviewsMainScreenAppState { export const mapStateToProps = (state: Store) => ({ reviews: getReviewListAsSectionList(state), archiveCount: getArchivedReviewListLength(state), + showOnboarding: getShowOnBoarding(state), }); export const mapDispatchToProps = (dispatch: Dispatch) => ({ addReview: (review: Review) => addReview(review)(dispatch), + hideOnboarding: () => + dispatch(settingsSlice.actions.toggleShowOnBoarding(false)), }); diff --git a/src/screens/review/details/index.tsx b/src/screens/review/details/index.tsx index 220edbc..a643d9c 100644 --- a/src/screens/review/details/index.tsx +++ b/src/screens/review/details/index.tsx @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/indent */ import React, { Component } from 'react'; +import moment from 'moment'; import { Alert, FlatList, View } from 'react-native'; import { connect } from 'react-redux'; import ViewPager from '@react-native-community/viewpager'; @@ -25,6 +26,7 @@ import resources from 'src/resources'; import EmptyState from 'src/components/empty-state'; import { withThrottle } from 'src/utils/timers'; import { LocalizationContext } from 'src/services/i18n'; +import { capitalize } from 'src/utils/strings'; import { ReviewDetailsState, @@ -210,6 +212,23 @@ class ReviewDetails extends Component { }); }; + moveToNextReminder = () => { + const { review } = this.state; + const { skipToNextReminder } = this.props; + const { translate, strings } = this.context; + const onAccept = () => { + skipToNextReminder( + review as Review, + translate(strings.timeForReviewProcess), + ); + this.fetchReview(); + }; + Alert.alert('', translate(strings.nextReminder), [ + { text: translate(strings.yes), onPress: onAccept }, + { text: translate(strings.no) }, + ]); + }; + renderEmptyLogList = () => { const { translate, strings } = this.context; return ( @@ -243,15 +262,51 @@ class ReviewDetails extends Component { return ; }; + renderBadges = () => { + const { review } = this.state; + const { translate, strings } = this.context; + const expired = moment(review.nextReminder) + .add(1, 'hour') + .isBefore(moment()); + return ( + + + {`${capitalize(translate(review.type!.toLowerCase()))}`} + + {review.archivedAt ? ( + + {`${translate(strings.archivedAt)} ${review.archivedAt}`} + + ) : null} + {expired ? ( + + {translate(strings.expired)} + + ) : null} + + ); + }; + renderDetails = () => { const { review } = this.state; const { logs } = this.props; const { translate, strings } = this.context; return ( - - {`${translate(strings.archivedAt)} ${review.archivedAt}`} - + {this.renderBadges()} {review.title} {translate(strings.averageTime)}:{'\n'} @@ -273,7 +328,23 @@ class ReviewDetails extends Component { }} /> - + + {capitalize( + moment(review.nextReminder).format('MMMM DD YYYY, h:mm a'), + )} + + + {translate(strings.skipToNextDate)} + { this.openLogs(1)} - style={{ padding: 20, paddingHorizontal: 80 }} + style={{ paddingBottom: 20, paddingHorizontal: 80 }} name="keyboard-arrow-up" size={38} /> + this.openLogs(1)} style={styles.averageText}> + {translate(strings.seeLogs)} + ); diff --git a/src/screens/review/details/props.ts b/src/screens/review/details/props.ts index bad6d45..e4722ec 100644 --- a/src/screens/review/details/props.ts +++ b/src/screens/review/details/props.ts @@ -5,6 +5,7 @@ import { getReview } from 'src/store/reviews/selector'; import { changeArchiveStateReview, deleteReview, + skipToNextReminder, } from 'src/store/reviews/actions'; import { Review, ReviewLog } from 'src/@types/index'; @@ -20,6 +21,7 @@ export interface ReviewDetailsProps extends ActionSheetProps { logs: { [key: string]: ReviewLog }; getReview(id: string): Review; deleteReview(id: string): Promise; + skipToNextReminder(review: Review, message: string): Promise; changeArchiveStateReview( id: string, review: Review, @@ -55,4 +57,6 @@ export const mapDispatchToProps = (dispatch: Dispatch) => ({ } return changeArchiveStateReview(id)(dispatch); }, + skipToNextReminder: (review: Review, message: string) => + skipToNextReminder(review, message)(dispatch), }); diff --git a/src/screens/review/details/styles.ts b/src/screens/review/details/styles.ts index 6a9bd04..68217e7 100644 --- a/src/screens/review/details/styles.ts +++ b/src/screens/review/details/styles.ts @@ -12,9 +12,11 @@ export default StyleSheet.create<{ playButton: ViewStyle; playButtonContainer: ViewStyle; averageText: TextStyle; + nextDate: TextStyle; title: TextStyle; firstPage: ViewStyle; badge: ViewStyle; + expiredBadge: ViewStyle; listHeaderComponent: ViewStyle; }>({ container: { backgroundColor: 'white', flex: 1 }, @@ -43,6 +45,11 @@ export default StyleSheet.create<{ lineHeight: 24, textAlign: 'center', }, + nextDate: { + fontSize: 20, + lineHeight: 24, + textAlign: 'center', + }, title: { fontSize: 40, lineHeight: 44, @@ -59,5 +66,14 @@ export default StyleSheet.create<{ marginVertical: 16, backgroundColor: colors.lynch, alignSelf: 'center', + paddingHorizontal: 8, + marginRight: 8, + }, + expiredBadge: { + fontWeight: 'bold', + alignSelf: 'center', + margin: 0, + padding: 0, + backgroundColor: '#004B67', }, }); diff --git a/src/screens/review/log/index.tsx b/src/screens/review/log/index.tsx index 6f8518d..539a12e 100644 --- a/src/screens/review/log/index.tsx +++ b/src/screens/review/log/index.tsx @@ -1,5 +1,5 @@ import moment from 'moment'; -import React, { useEffect, memo } from 'react'; +import React, { useEffect, memo, useState, useCallback } from 'react'; import { connect, useDispatch } from 'react-redux'; import { View } from 'react-native'; import { @@ -28,6 +28,7 @@ import { } from 'src/@types/index'; import { deleteLog } from 'src/store/logs/actions'; import { LocalizationContext } from 'src/services/i18n'; +import { isAnswerEmpty } from 'src/utils/questions'; import styles from './styles'; import { @@ -64,10 +65,10 @@ const formatAnswer = (type: ReviewQuestionType, value?: string) => { return value; }; -const renderOption = (log: ReviewQuestion) => ( +const renderOption = (log: ReviewQuestion, showHidden: boolean) => ( option: ReviewQuestionOption, ) => { - if (!option.value) { + if (!option.value && !showHidden && !(log.type === ReviewQuestionType.List)) { return null; } return ( @@ -92,7 +93,11 @@ const renderOption = (log: ReviewQuestion) => ( ); }; -function LogQuestion(log: ReviewQuestion) { +function LogQuestion(log: ReviewQuestion, showHidden: boolean = false) { + if (!showHidden && isAnswerEmpty(log.answer)) { + return null; + } + if ( log.type === ReviewQuestionType.Choice || log.type === ReviewQuestionType.List || @@ -102,7 +107,7 @@ function LogQuestion(log: ReviewQuestion) { {log.q} - {log.answer?.options?.map(renderOption(log))} + {log.answer?.options?.map(renderOption(log, showHidden))} ); } @@ -125,24 +130,36 @@ function ReviewLogScreenProcess(props: ReviewLogScreenProps) { const { showActionSheetWithOptions } = useActionSheet(); const dispatch = useDispatch(); const { translate, strings } = React.useContext(LocalizationContext); + const [showHidden, setShowHidden] = useState(false); const onDelete = () => { deleteLog(review.id, log.id)(dispatch); navigation.pop(); }; + const onToggleHidden = useCallback(() => { + setShowHidden(!showHidden); + }, [showHidden]); + const openOptionsMenu = () => { const destructiveButtonIndex = 0; const cancelButtonIndex = 3; const mapOption: { [key: number]: Function } = { 0: onDelete, + 1: onToggleHidden, }; - const contextMenuOptions = [translate(strings.delete)]; + const contextMenuOptions = [ + translate(strings.delete), + showHidden + ? translate(strings.hideHidden) + : translate(strings.showHidden), + ]; const contextMenuOptionIcons = [ , + , ]; showActionSheetWithOptions( @@ -166,7 +183,7 @@ function ReviewLogScreenProcess(props: ReviewLogScreenProps) { headerRightIcon: 'dots-vertical', headerRightOnPress: openOptionsMenu, }); - }, []); + }, [showHidden]); return ( {moment(log.date).format('MMMM Do YYYY, h:mm:ss a')} - {log.questions.map(LogQuestion)} + {log.questions.map((q: ReviewQuestion) => + LogQuestion(q, showHidden), + )} diff --git a/src/screens/settings/index.tsx b/src/screens/settings/index.tsx index 0b1ac58..4735f92 100644 --- a/src/screens/settings/index.tsx +++ b/src/screens/settings/index.tsx @@ -7,6 +7,7 @@ import { useDispatch, useSelector } from 'react-redux'; import ScreenContainer from 'src/components/screen-container'; import Dropdown from 'src/components/dropdown/with-description'; +import OnBoarding from 'src/containers/onboarding'; import { DropdownOption } from 'src/@types'; import { SCREEN_NAMES } from 'src/navigation/constants'; @@ -22,9 +23,13 @@ import styles from './styles'; function Setting({ navigation }: any) { const dispatch = useDispatch(); - const { user, development, language, notifications } = useSelector( - settingsStoreSelector, - ); + const { + user, + development, + language, + notifications, + showOnBoarding, + } = useSelector(settingsStoreSelector); const { setLocale, translate, strings } = React.useContext( LocalizationContext, @@ -141,9 +146,22 @@ function Setting({ navigation }: any) { /> )} /> + + dispatch(settingsSlice.actions.toggleShowOnBoarding(true)) + } + /> + + dispatch(settingsSlice.actions.toggleShowOnBoarding(false)) + } + /> ); } diff --git a/src/services/i18n/translations/english.json b/src/services/i18n/translations/english.json index bdab509..49ae64f 100644 --- a/src/services/i18n/translations/english.json +++ b/src/services/i18n/translations/english.json @@ -105,5 +105,26 @@ "friday": "friday", "saturday": "saturday", "sunday": "sunday", - "item": "item" + "item": "item", + "showOnboarding": "Show Onboarding", + "onboardingTitle1": "Retrospectives", + "onboardingTitle2": "Create reviews", + "onboardingTitle3": "Review Cadence", + "onboardingTitle4": "Notifications", + "onboardingTitle5": "Enjoy!", + "onboardingDescription1": "Evaluate your productivity in a way that helps you improve over time! \n\nCreate custom reviews!", + "onboardingDescription2": "A Review is a custom questionary. When a review is done, a log is created with the provided answers.", + "onboardingDescription3": "Reviews Type determines the recurrence of the review.\n\nDaily, Weekly, Monthly, Yearly!", + "onboardingDescription4": "Notifications are enabled by default, you can disable them on the settings screen.", + "onboardingDescription5": "Start creating a \n\"How was your week?\" \nweekly review. \n\n<3", + "next": "Next", + "close": "Close", + "skip": "Skip", + "helpTranslate": "Help Translate", + "reportBug": "Report an issue", + "nextReminder": "Are you sure you want to skip to the next reminder?", + "skipToNextDate": "Skip to next date", + "seeLogs": "See logs", + "showHidden": "Show Empty", + "hideHidden": "Hide Empty" } diff --git a/src/services/i18n/translations/spanish.json b/src/services/i18n/translations/spanish.json index af715dc..3c78df3 100644 --- a/src/services/i18n/translations/spanish.json +++ b/src/services/i18n/translations/spanish.json @@ -52,7 +52,7 @@ "required": "requerido", "needToBeFilled": "Debe completar todas las preguntas requeridas.", "discardReview": "¿Descartar evaluación?", - "discardReviewMessage": "Estás en medio de una evaluación, ¿estás seguro de que quieres irte?", + "discardReviewMessage": "Estás en medio de una evaluación, ¿Deseas salir?", "discard": "Descartar", "dontLeave": "Seguir", "finish": "Terminar", @@ -105,5 +105,26 @@ "friday": "viernes", "saturday": "sábado", "sunday": "domingo", - "item": "elemento" + "item": "elemento", + "showOnboarding": "Mostral Tutorial", + "onboardingTitle1": "Retrospectivas", + "onboardingTitle2": "Crea evaluaciones", + "onboardingTitle3": "Recurrencia", + "onboardingTitle4": "Notificaciones", + "onboardingTitle5": "Disfruta!", + "onboardingDescription1": "¡Evalúa tu productividad de una manera que te ayude a mejorar con el paso del tiempo! \n\n¡Crea evaluaciones personalizadas!", + "onboardingDescription2": "Una evaluación es un cuestionario personalizado. Cuando se completa una evaluación, se crea un registro con las respuestas proporcionadas.", + "onboardingDescription3": "El tipo de evaluación determina la repetición de la misma. \n\n¡Diaria, Semanal, Mensual, Anual!", + "onboardingDescription4": "Las notificaciones están habilitadas por defecto, pueden ser deshabilitarlas en la pantalla de ajustes.", + "onboardingDescription5": "Comienza creando una\nevaluacion semanal de\n\n\"¿Cómo estuvo tu semana?\"\n\n<3", + "next": "Continuar", + "close": "Cerrar", + "skip": "Saltar", + "helpTranslate": "Ayuda a traducir", + "reportBug": "Reporta un problema", + "nextReminder": "¿Quieres saltar al siguiente recordatorio?", + "skipToNextDate": "Saltar fecha", + "seeLogs": "Ver registros", + "showHidden": "Mostrar vacías", + "hideHidden": "Ocultar vacías" } diff --git a/src/store/index.ts b/src/store/index.ts index 71aa095..19aafce 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -37,7 +37,7 @@ export const reducers = combineReducers({ const persistConfig = { key: 'root', storage, - version: 2, + version: 3, migrate: createMigrate(migrations as any, { debug: false }), }; diff --git a/src/store/migrations/3.ts b/src/store/migrations/3.ts new file mode 100644 index 0000000..596a02a --- /dev/null +++ b/src/store/migrations/3.ts @@ -0,0 +1,11 @@ +import { Store } from 'src/@types/store'; + +export default function(state: Store) { + return { + ...state, + settings: { + ...state.settings, + showOnBoarding: true, + }, + }; +} diff --git a/src/store/migrations/index.ts b/src/store/migrations/index.ts index b93d382..6189f92 100644 --- a/src/store/migrations/index.ts +++ b/src/store/migrations/index.ts @@ -1,9 +1,11 @@ import _0 from './0'; import _1 from './1'; import _2 from './2'; +import _3 from './3'; export default { 0: _0, 1: _1, 2: _2, + 3: _3, }; diff --git a/src/store/reviews/actions.ts b/src/store/reviews/actions.ts index 8104ada..f108b27 100644 --- a/src/store/reviews/actions.ts +++ b/src/store/reviews/actions.ts @@ -1,6 +1,11 @@ import { Dispatch } from 'redux'; import { Review } from 'src/@types/index'; +import notificationSlice from 'src/store/notifications/reducer'; +import { addReviewScheduledNotification } from 'src/store/notifications/actions'; +import { mapReviewToNotificationPayload } from 'src/utils/notifications'; +import { moveDateToPresent } from 'src/utils/time'; + import slice from './reducer'; export function addReview(review: Review) { @@ -26,3 +31,24 @@ export function changeArchiveStateReview(id: string) { return dispatch(slice.actions.changeArchiveStateReview(id)); }; } + +export function skipToNextReminder(review: Review, message: string) { + return async (dispatch: Dispatch) => { + const updated: Review = { + ...review, + nextReminder: moveDateToPresent(review.nextReminder, review.type), + }; + dispatch( + notificationSlice.actions.deleteNotifications({ reviewId: updated.id }), + ); + addReviewScheduledNotification( + mapReviewToNotificationPayload(updated, message, true), + )(dispatch); + return dispatch( + slice.actions.skipToNextReminder({ + id: updated.id, + nextReminder: updated.nextReminder, + }), + ); + }; +} diff --git a/src/store/reviews/reducer.ts b/src/store/reviews/reducer.ts index 9d3f10d..e5b6487 100644 --- a/src/store/reviews/reducer.ts +++ b/src/store/reviews/reducer.ts @@ -12,6 +12,7 @@ import { ReviewsState } from 'src/@types/store'; import { Review, ReviewLog } from 'src/@types'; import { addLogAction, deleteLogAction } from 'src/store/shared/actions'; import { getNextDate } from 'src/utils/reviews'; +import { moveDateToPresent } from 'src/utils/time'; import { ReviewInitialState } from './state'; @@ -54,6 +55,19 @@ function addLog( ).format(); } +function skipToNextReminder( + state: ReviewsState, + { payload }: PayloadAction<{ id: string; nextReminder: string }>, +) { + state.reviews[payload.id].nextReminder = getNextDate({ + ...state.reviews[payload.id], + nextReminder: moveDateToPresent( + payload.nextReminder, + state.reviews[payload.id].type, + ), + }).format(); +} + function deleteLog( state: ReviewsState, { payload }: PayloadAction<{ reviewId: string; logId: string }>, @@ -77,6 +91,7 @@ export default createSlice({ changeArchiveStateReview, deleteReview, editReview, + skipToNextReminder, }, extraReducers: builder => { builder.addCase(addLogAction, addLog); diff --git a/src/store/settings/selectors.ts b/src/store/settings/selectors.ts index 51b4edf..1e2c137 100644 --- a/src/store/settings/selectors.ts +++ b/src/store/settings/selectors.ts @@ -23,7 +23,7 @@ export const getUseDarkMode = createSelector( export const getShowOnBoarding = createSelector( settingsStoreSelector, - store => store.showOnBoarding || {}, + store => store.showOnBoarding || false, ); export const getUseRewards = createSelector( diff --git a/src/theme/colors.ts b/src/theme/colors.ts index bc37665..675c8e4 100644 --- a/src/theme/colors.ts +++ b/src/theme/colors.ts @@ -7,6 +7,7 @@ export default { yearly: '#F8BBD0', custom: '#B2DFDB', brown: '#5D4037', + lightBrown: '#815355', lynch: '#607D8B', pistonBlue: '#46ADDE', shamrock: '#31C4A8', diff --git a/src/utils/time.ts b/src/utils/time.ts index 356316a..3dc0f8c 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,4 +1,4 @@ -import { DayOfTheWeek } from 'src/@types/index'; +import { DayOfTheWeek, ReviewType } from 'src/@types/index'; import moment from 'moment'; import { translate } from 'src/services/i18n/index'; @@ -96,3 +96,30 @@ export const formatReviewDate = (date: string, locale: string): string => { return dateValue.calendar().split(languageSplitter[locale])[0]; }; + +// FIXME Sometimes on weekly review, the diff is less and the review stays near today but in the past +export const moveDateToPresent = (date: string, type: ReviewType): string => { + const a = moment(date); + const b = moment(); + if (type === ReviewType.daily) { + const days = b.diff(a, 'days'); + if (days < 0) return date; + return a.add(days, 'days').format(); + } + if (type === ReviewType.weekly) { + const weeks = b.diff(a, 'weeks'); + if (weeks < 0) return date; + return a.add(weeks, 'weeks').format(); + } + if (type === ReviewType.monthly) { + const months = b.diff(a, 'months'); + if (months < 0) return date; + return a.add(moment.duration({ M: months }), 'M').format(); + } + if (type === ReviewType.yearly) { + const year = b.diff(a, 'years'); + if (year < 0) return date; + return a.add(year, 'year').format(); + } + return date; +}; diff --git a/yarn.lock b/yarn.lock index 320a4ce..178c758 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2363,6 +2363,11 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +abs-svg-path@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz#df601c8e8d2ba10d4a76d625e236a9a39c2723bf" + integrity sha1-32Acjo0roQ1KdtYl4japo5wnI78= + absolute-path@^0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/absolute-path/-/absolute-path-0.0.0.tgz#a78762fbdadfb5297be99b15d35a785b2f095bf7" @@ -7640,6 +7645,13 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-svg-path@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz#0e614eca23c39f0cffe821d6be6cd17e569a766c" + integrity sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg== + dependencies: + svg-arc-to-cubic-bezier "^3.0.0" + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -8031,6 +8043,11 @@ parse-node-version@^1.0.0: resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== +parse-svg-path@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb" + integrity sha1-en7A0esG+lMlx9PgCbhZoJtdSes= + parse5@5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" @@ -8550,6 +8567,15 @@ react-native-reanimated@^1.13.1: dependencies: fbjs "^1.0.0" +react-native-redash@^16.0.8: + version "16.0.8" + resolved "https://registry.yarnpkg.com/react-native-redash/-/react-native-redash-16.0.8.tgz#734b016f17dc747560891502b6c5db5e04a17850" + integrity sha512-2Opu4ls31HRGQnz11iq4cnJhCz0GnasnnZgihAlCsVFc9ofa9+47hbc1xo/WkN0OZ5X5KKo869soyfYQgIxy+w== + dependencies: + abs-svg-path "^0.1.1" + normalize-svg-path "^1.0.1" + parse-svg-path "^0.1.2" + react-native-safe-area-context@^3.1.8: version "3.1.8" resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-3.1.8.tgz#7a9883aa0f38f9c77d4bef2b89e4e285bc1024a3" @@ -9801,6 +9827,11 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" +svg-arc-to-cubic-bezier@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz#390c450035ae1c4a0104d90650304c3bc814abe6" + integrity sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g== + symbol-observable@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"