├── .watchmanconfig ├── .tool-versions ├── .env ├── .env.example ├── .bundle └── config ├── app.json ├── src ├── providers │ ├── alert-message │ │ ├── index.ts │ │ ├── AlertMessage.tsx │ │ └── AlertMessage.styles.ts │ ├── index.ts │ ├── tmdb-image-qualities │ │ ├── qualities │ │ │ ├── large │ │ │ │ ├── index.ts │ │ │ │ ├── low.ts │ │ │ │ ├── high.ts │ │ │ │ ├── medium.ts │ │ │ │ └── very-high.ts │ │ │ ├── medium │ │ │ │ ├── index.ts │ │ │ │ ├── low.ts │ │ │ │ ├── high.ts │ │ │ │ ├── medium.ts │ │ │ │ └── very-high.ts │ │ │ ├── small │ │ │ │ ├── index.ts │ │ │ │ ├── low.ts │ │ │ │ ├── high.ts │ │ │ │ ├── medium.ts │ │ │ │ └── very-high.ts │ │ │ ├── xlarge │ │ │ │ ├── index.ts │ │ │ │ ├── high.ts │ │ │ │ ├── low.ts │ │ │ │ ├── medium.ts │ │ │ │ └── very-high.ts │ │ │ └── xsmall │ │ │ │ ├── index.ts │ │ │ │ ├── low.ts │ │ │ │ ├── high.ts │ │ │ │ ├── medium.ts │ │ │ │ └── very-high.ts │ │ └── types.ts │ ├── graphql-client │ │ └── GraphQLClient.tsx │ └── theme │ │ └── ThemeProvider.tsx ├── styles │ ├── themes │ │ ├── index.ts │ │ ├── light.ts │ │ └── dark.ts │ ├── constants.ts │ ├── border-radius.ts │ └── metrics.ts ├── i18n │ ├── supported-languages.ts │ ├── language-detection.ts │ └── index.ts ├── components │ ├── stacks │ │ ├── home │ │ │ ├── HomeStack.tsx │ │ │ └── routes │ │ │ │ └── route-params-types.ts │ │ ├── common-screens │ │ │ ├── images-gallery │ │ │ │ ├── screen │ │ │ │ │ ├── ImagesGallery.styles.ts │ │ │ │ │ └── components │ │ │ │ │ │ ├── thumbs-list │ │ │ │ │ │ ├── ThumbsList.styles.ts │ │ │ │ │ │ ├── thumbs-list-item │ │ │ │ │ │ │ └── ThumbsListItem.tsx │ │ │ │ │ │ └── ThumbsList.tsx │ │ │ │ │ │ └── images-list │ │ │ │ │ │ ├── ImagesList.styles.ts │ │ │ │ │ │ └── images-list-item │ │ │ │ │ │ ├── ImagesListItem.styles.ts │ │ │ │ │ │ └── ImagesListItem.tsx │ │ │ │ └── routes │ │ │ │ │ └── route-params-types.ts │ │ │ ├── search │ │ │ │ ├── Search.styles.ts │ │ │ │ ├── components │ │ │ │ │ ├── search-bar │ │ │ │ │ │ ├── use-search-bar.ts │ │ │ │ │ │ ├── SearchBar.styles.tsx │ │ │ │ │ │ └── SearchBar.tsx │ │ │ │ │ └── recent-searches │ │ │ │ │ │ ├── RecentSearches.styles.ts │ │ │ │ │ │ ├── RecentSearches.tsx │ │ │ │ │ │ └── recent-searchers-list-item │ │ │ │ │ │ └── RecentSearchesListItem.tsx │ │ │ │ ├── debounce.ts │ │ │ │ ├── types.ts │ │ │ │ └── search-config │ │ │ │ │ ├── search-movies-config.ts │ │ │ │ │ ├── search-config.ts │ │ │ │ │ ├── search-tv-shows-config.ts │ │ │ │ │ └── search-famous-config.ts │ │ │ ├── media-details │ │ │ │ ├── common │ │ │ │ │ ├── participants-list │ │ │ │ │ │ ├── ParticipantsList.styles.ts │ │ │ │ │ │ ├── ParticipantsList.tsx │ │ │ │ │ │ └── participants-list-item │ │ │ │ │ │ │ └── ParticipantsListItem.tsx │ │ │ │ │ ├── videos │ │ │ │ │ │ ├── use-videos.ts │ │ │ │ │ │ ├── Videos.styles.ts │ │ │ │ │ │ └── Videos.tsx │ │ │ │ │ ├── media-info │ │ │ │ │ │ ├── MediaInfo.styles.ts │ │ │ │ │ │ └── MediaInfo.tsx │ │ │ │ │ ├── loading │ │ │ │ │ │ └── MediaDetailsLoading.tsx │ │ │ │ │ ├── background-image │ │ │ │ │ │ ├── BackgroundImage.tsx │ │ │ │ │ │ ├── BackgroundImage.styles.ts │ │ │ │ │ │ └── use-background-image.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── tv-show-details │ │ │ │ │ ├── screen │ │ │ │ │ │ └── components │ │ │ │ │ │ │ └── seasons-section │ │ │ │ │ │ │ ├── SeasonsSection.styles.ts │ │ │ │ │ │ │ └── SeasonsSection.tsx │ │ │ │ │ └── routes │ │ │ │ │ │ └── route-params-types.ts │ │ │ │ └── movie-details │ │ │ │ │ └── routes │ │ │ │ │ └── route-params-types.ts │ │ │ ├── famous-details │ │ │ │ ├── components │ │ │ │ │ ├── biography │ │ │ │ │ │ ├── Biography.styles.ts │ │ │ │ │ │ └── Biography.tsx │ │ │ │ │ ├── header │ │ │ │ │ │ ├── birthday-text │ │ │ │ │ │ │ └── BirthDayText.tsx │ │ │ │ │ │ └── header-loading-placeholder │ │ │ │ │ │ │ ├── HeaderLoadingPlaceholder.styles.ts │ │ │ │ │ │ │ └── HeaderLoadingPlaceholder.tsx │ │ │ │ │ └── death-day │ │ │ │ │ │ ├── DeathDay.tsx │ │ │ │ │ │ ├── use-death-day.ts │ │ │ │ │ │ ├── DeathDay-en.test.tsx │ │ │ │ │ │ ├── DeathDay-es.test.tsx │ │ │ │ │ │ ├── DeathDay-pt.test.tsx │ │ │ │ │ │ └── DeathDay.styles.ts │ │ │ │ ├── FamousDetails.styles.ts │ │ │ │ └── routes │ │ │ │ │ └── route-params-types.ts │ │ │ ├── index.ts │ │ │ └── tv-show-season │ │ │ │ ├── routes │ │ │ │ └── route-params-types.ts │ │ │ │ └── screen │ │ │ │ ├── scroll-with-animated-header-params.ts │ │ │ │ └── components │ │ │ │ ├── tv-show-season-loading │ │ │ │ ├── TVShowSeasonLoading.styles.ts │ │ │ │ └── TVShowSeasonLoading.tsx │ │ │ │ └── episode-details-modal │ │ │ │ └── EpisodeDetailsModal.styles.ts │ │ ├── quiz │ │ │ └── screens │ │ │ │ ├── setup-questions │ │ │ │ ├── use-setup-questions │ │ │ │ │ ├── options │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ ├── categories.ts │ │ │ │ │ │ └── difficulties.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── components │ │ │ │ │ ├── setup-questions-modal │ │ │ │ │ │ ├── SetupQuestionModal.styles.tsx │ │ │ │ │ │ └── option-list-item │ │ │ │ │ │ │ ├── OptionListItem.styles.ts │ │ │ │ │ │ │ └── OptionListItem.tsx │ │ │ │ │ ├── number-of-questions │ │ │ │ │ │ ├── NumberOfQuestionts.styles.tsx │ │ │ │ │ │ └── use-number-of-questions.ts │ │ │ │ │ └── choose-option-section │ │ │ │ │ │ ├── ChooseOptionSection.styles.tsx │ │ │ │ │ │ └── ChooseOptionSection.tsx │ │ │ │ └── SetupQuestions.styles.ts │ │ │ │ ├── results │ │ │ │ ├── Results.styles.ts │ │ │ │ ├── components │ │ │ │ │ └── result-list-item │ │ │ │ │ │ ├── use-result-list-item.ts │ │ │ │ │ │ └── ResultListItem.tsx │ │ │ │ └── Results.tsx │ │ │ │ ├── quiz │ │ │ │ ├── Quiz.styles.ts │ │ │ │ ├── Quiz.tsx │ │ │ │ └── use-quiz.ts │ │ │ │ └── questions │ │ │ │ └── components │ │ │ │ ├── question-wrapper │ │ │ │ ├── QuestionWrapper.tsx │ │ │ │ ├── QuestionWrapper.styles.ts │ │ │ │ └── QuestionWrapper.test.tsx │ │ │ │ ├── multi-choice-question │ │ │ │ ├── use-multi-choice-question.ts │ │ │ │ └── multi-choice-question-list-item │ │ │ │ │ └── MultiChoiceQuestionListItem.styles.ts │ │ │ │ └── boolean-question │ │ │ │ ├── use-boolean-question.ts │ │ │ │ └── BooleanQuestion.styles.ts │ │ ├── index.ts │ │ ├── famous │ │ │ ├── screens │ │ │ │ └── trending-famous │ │ │ │ │ └── TrendingFamous.styles.ts │ │ │ └── routes │ │ │ │ └── route-params-types.ts │ │ └── news │ │ │ ├── screens │ │ │ └── news │ │ │ │ ├── components │ │ │ │ ├── news-list-item │ │ │ │ │ ├── date-diff │ │ │ │ │ │ ├── DateDiff.styles.ts │ │ │ │ │ │ └── DateDiff.tsx │ │ │ │ │ ├── use-news-list-item.ts │ │ │ │ │ ├── news-image │ │ │ │ │ │ └── NewsImage.styles.ts │ │ │ │ │ └── NewsListItem.tsx │ │ │ │ ├── news-loading │ │ │ │ │ ├── NewsLoading.styles.ts │ │ │ │ │ ├── NewsLoading.test.tsx │ │ │ │ │ └── NewsLoading.tsx │ │ │ │ └── languages-filter-modal │ │ │ │ │ ├── language-filter-list │ │ │ │ │ ├── filter-languages │ │ │ │ │ │ └── use-news-filter-languages.ts │ │ │ │ │ └── LanguageFilterList.tsx │ │ │ │ │ └── LanguagesFilterModal.tsx │ │ │ │ └── News.styles.ts │ │ │ └── routes │ │ │ ├── route-params-types.ts │ │ │ └── stack-routes.tsx │ └── common │ │ ├── svg-icon │ │ ├── index.ts │ │ └── SVGIcon.tsx │ │ ├── media-item-description │ │ └── MediaItemDescription.styles.ts │ │ ├── tmdb-image │ │ └── TMDBImage.styles.ts │ │ ├── images-list │ │ ├── ImagesList.styles.ts │ │ ├── images-list-item │ │ │ ├── ImageListItem.styles.ts │ │ │ └── ImageListItem.tsx │ │ ├── ImagesList.tsx │ │ └── use-images-list.ts │ │ ├── media-horizontal-list │ │ └── MediaHorizontalList.style.ts │ │ ├── stars-votes │ │ └── StarsVotes.styles.ts │ │ ├── section │ │ ├── Section.styles.tsx │ │ └── Section.tsx │ │ ├── status-bar │ │ ├── StatusBar.tsx │ │ └── use-status-bar.ts │ │ ├── paginated-list-header │ │ ├── PaginatedListHeader.styles.ts │ │ ├── PaginatedListHeader.tsx │ │ └── PaginatedListHeader.test.tsx │ │ ├── rounded-button │ │ ├── RoundedButton.tsx │ │ └── RoundedButton.styles.ts │ │ ├── scroll-with-animated-header │ │ └── use-scroll-with-animated-header.ts │ │ ├── default-tmdb-list-loading │ │ ├── DefaultTMDBListLoading.styles.ts │ │ └── DefaultTMDBListLoading.tsx │ │ ├── paginated-list-footer │ │ ├── PaginatedListFooter.styles.ts │ │ └── PaginatedListFooter.tsx │ │ ├── modal-select-button │ │ ├── ModalSelectButton.tsx │ │ └── ModalSelectButton.styles.ts │ │ ├── default-tmdb-list-item │ │ └── DefaultTMDBListItem.tsx │ │ ├── header-icon-button │ │ ├── HeaderIconButton.tsx │ │ └── HeaderIconButton.styles.ts │ │ ├── loading-placeholder │ │ ├── LoadingPlaceholder.tsx │ │ └── use-loading-placeholder.ts │ │ ├── advice │ │ └── Advice.tsx │ │ └── media-list-item │ │ └── MediaListItem.styles.ts ├── navigation │ ├── index.ts │ ├── components │ │ ├── AndroidNavigationBar.android.tsx │ │ ├── tab-navigator │ │ │ ├── TabNavigator.styles.ts │ │ │ ├── TabNavigator.tsx │ │ │ ├── tabs.ts │ │ │ └── tab-navigator-item │ │ │ │ └── TabNavigatorItem.styles.ts │ │ ├── HeaderTitle.tsx │ │ └── Tabs.tsx │ ├── navigation-utils.ts │ ├── Navigation.tsx │ └── use-default-header.tsx ├── hooks │ ├── index.ts │ ├── use-tmdb-image-uri │ │ └── use-tmdb-image-uri.ts │ └── use-translation │ │ └── use-translation.ts ├── utils │ ├── format-currency │ │ ├── format-currency.ts │ │ └── format-currency.test.ts │ ├── format-date │ │ ├── format-date.ts │ │ └── format-date.test.ts │ ├── index.ts │ ├── status-bar-height │ │ ├── get-statusbar-height.ts │ │ └── get-statusbar-height-ios.test.ts │ ├── is-equals-or-larger-than-iphonex │ │ └── is-equals-or-larger-than-iphonex.ts │ ├── render-svg-icon-conditionally │ │ └── render-svg-icon-conditionally.tsx │ └── storage │ │ └── storage.ts ├── types │ └── index.ts └── App.tsx ├── .DS_Store ├── .prettierignore ├── __mocks__ ├── react-native-restart.js ├── Dimensions.js ├── index.ts ├── apollo.tsx ├── MockedNavigator.tsx └── utils.ts ├── android ├── app │ ├── debug.keystore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ └── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ ├── assets │ │ │ │ └── fonts │ │ │ │ │ ├── CircularStd-Black.otf │ │ │ │ │ ├── CircularStd-Bold.otf │ │ │ │ │ ├── CircularStd-Book.otf │ │ │ │ │ └── CircularStd-Medium.otf │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── cinetasty │ │ │ │ └── MainActivity.java │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── release │ │ │ └── java │ │ │ └── com │ │ │ └── cinetasty │ │ │ └── ReactNativeFlipper.java │ └── proguard-rules.pro ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle ├── link-assets-manifest.json └── build.gradle ├── assets └── fonts │ ├── CircularStd-Bold.otf │ ├── CircularStd-Book.otf │ ├── CircularStd-Black.otf │ └── CircularStd-Medium.otf ├── ios ├── CineTasty │ ├── Images.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ └── AppDelegate.mm ├── CineTasty.xcworkspace │ └── contents.xcworkspacedata ├── .xcode.env ├── link-assets-manifest.json └── CineTastyTests │ └── Info.plist ├── .eslintignore ├── .husky └── pre-commit ├── react-native.config.js ├── .prettierrc.js ├── .editorconfig ├── Gemfile ├── index.js ├── metro.config.js ├── jest.config.js ├── jest.setup.js ├── .eslintrc.js ├── .github └── workflows │ └── ci.yaml ├── tsconfig.json ├── .gitignore └── babel.config.js /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 16.20.2 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SERVER_URL=http://192.168.18.8:3000/ 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SERVER_URL= 2 | -------------------------------------------------------------------------------- /.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CineTasty", 3 | "displayName": "CineTasty" 4 | } 5 | -------------------------------------------------------------------------------- /src/providers/alert-message/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AlertMessageContext'; 2 | -------------------------------------------------------------------------------- /src/styles/themes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dark'; 2 | export * from './light'; 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/.DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | android 3 | ios 4 | coverage 5 | src/types/schema.ts 6 | -------------------------------------------------------------------------------- /src/i18n/supported-languages.ts: -------------------------------------------------------------------------------- 1 | export const supportedLanguages = ['en', 'es', 'pt']; 2 | -------------------------------------------------------------------------------- /__mocks__/react-native-restart.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Restart: jest.fn(), 3 | restart: jest.fn(), 4 | }; 5 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CineTasty 3 | 4 | -------------------------------------------------------------------------------- /assets/fonts/CircularStd-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/assets/fonts/CircularStd-Bold.otf -------------------------------------------------------------------------------- /assets/fonts/CircularStd-Book.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/assets/fonts/CircularStd-Book.otf -------------------------------------------------------------------------------- /ios/CineTasty/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /assets/fonts/CircularStd-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/assets/fonts/CircularStd-Black.otf -------------------------------------------------------------------------------- /assets/fonts/CircularStd-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/assets/fonts/CircularStd-Medium.otf -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | android 3 | ios 4 | coverage 5 | *.test.ts 6 | *.test.tsx 7 | *.spec.tsx 8 | src/types/schema.ts 9 | 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run prettier:fix 5 | npm run lint:fix 6 | npm run test 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ios/CineTasty/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | project: { 3 | ios: {}, 4 | android: {}, 5 | }, 6 | assets: ['./assets/fonts'], 7 | }; 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/CircularStd-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/assets/fonts/CircularStd-Black.otf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/CircularStd-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/assets/fonts/CircularStd-Bold.otf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/CircularStd-Book.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/assets/fonts/CircularStd-Book.otf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/CircularStd-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/assets/fonts/CircularStd-Medium.otf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/components/stacks/home/HomeStack.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | 4 | export const HomeStack = () => HomeStack; 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: true, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /__mocks__/Dimensions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Dimensions = { 4 | get: jest.fn().mockReturnValue({ width: 100, height: 100 }), 5 | }; 6 | 7 | module.exports = Dimensions; 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steniowagner/cine-tasty-mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | gem 'cocoapods', '~> 1.12' 7 | -------------------------------------------------------------------------------- /src/components/common/svg-icon/index.ts: -------------------------------------------------------------------------------- 1 | export { type SVGIconProps, SVGIcon } from './SVGIcon'; 2 | export { type FlagsIcons, flags } from './flags'; 3 | export { type Icons } from './icons'; 4 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/images-gallery/screen/ImagesGallery.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Wrapper = styled.View` 4 | flex: 1; 5 | `; 6 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/use-setup-questions/options/index.ts: -------------------------------------------------------------------------------- 1 | export { difficulties } from './difficulties'; 2 | export { categories } from './categories'; 3 | export { types } from './types'; 4 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/components/setup-questions-modal/SetupQuestionModal.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Wrapper = styled.View` 4 | flex: 1; 5 | `; 6 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './theme/ThemeProvider'; 2 | export { GraphQLClient } from './graphql-client/GraphQLClient'; 3 | export * from './alert-message'; 4 | export * from './tmdb-image-qualities/TMDBImageQualities'; 5 | -------------------------------------------------------------------------------- /src/components/stacks/index.ts: -------------------------------------------------------------------------------- 1 | export { HomeStack } from './home/HomeStack'; 2 | export { FamousStack } from './famous/routes/stack-routes'; 3 | export { QuizStack } from './quiz/routes/stack-routes'; 4 | export { NewsStack } from './news/routes/stack-routes'; 5 | -------------------------------------------------------------------------------- /ios/CineTasty/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/navigation/index.ts: -------------------------------------------------------------------------------- 1 | export { Navigation } from './Navigation'; 2 | export { Routes } from './routes'; 3 | export * from './navigation-utils'; 4 | export { HeaderTitle } from './components/HeaderTitle'; 5 | export { useDefaultHeader } from './use-default-header'; 6 | -------------------------------------------------------------------------------- /src/styles/constants.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | defaultShadow: { 3 | shadowColor: '#000000', 4 | shadowOffset: { 5 | width: 0, 6 | height: 2, 7 | }, 8 | shadowOpacity: 0.25, 9 | shadowRadius: 3.84, 10 | elevation: 5, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/common/media-item-description/MediaItemDescription.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const ExpandableReadButton = styled.TouchableOpacity` 4 | margin-top: ${({ theme }) => theme.metrics.md}px; 5 | align-self: flex-end; 6 | `; 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'CineTasty' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/@react-native/gradle-plugin') 5 | -------------------------------------------------------------------------------- /src/components/common/tmdb-image/TMDBImage.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const sheet = StyleSheet.create({ 4 | fallbackImage: { 5 | justifyContent: 'center', 6 | alignItems: 'center', 7 | position: 'absolute', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/large/index.ts: -------------------------------------------------------------------------------- 1 | import low from './low'; 2 | import medium from './medium'; 3 | import high from './high'; 4 | import veryHigh from './very-high'; 5 | 6 | export default { 7 | low, 8 | medium, 9 | high, 10 | veryHigh, 11 | }; 12 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/medium/index.ts: -------------------------------------------------------------------------------- 1 | import low from './low'; 2 | import medium from './medium'; 3 | import high from './high'; 4 | import veryHigh from './very-high'; 5 | 6 | export default { 7 | low, 8 | medium, 9 | high, 10 | veryHigh, 11 | }; 12 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/small/index.ts: -------------------------------------------------------------------------------- 1 | import low from './low'; 2 | import medium from './medium'; 3 | import high from './high'; 4 | import veryHigh from './very-high'; 5 | 6 | export default { 7 | low, 8 | medium, 9 | high, 10 | veryHigh, 11 | }; 12 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/xlarge/index.ts: -------------------------------------------------------------------------------- 1 | import low from './low'; 2 | import medium from './medium'; 3 | import high from './high'; 4 | import veryHigh from './very-high'; 5 | 6 | export default { 7 | low, 8 | medium, 9 | high, 10 | veryHigh, 11 | }; 12 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/xsmall/index.ts: -------------------------------------------------------------------------------- 1 | import low from './low'; 2 | import medium from './medium'; 3 | import high from './high'; 4 | import veryHigh from './very-high'; 5 | 6 | export default { 7 | low, 8 | medium, 9 | high, 10 | veryHigh, 11 | }; 12 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/Search.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | import metrics from '@styles/metrics'; 4 | 5 | export const sheet = StyleSheet.create({ 6 | contentContainerStyle: { 7 | paddingTop: metrics.md, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/large/low.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const low: MappingImageTypeToImageSize = { 4 | poster: 'w92', 5 | backdrop: 'w300', 6 | still: 'w185', 7 | profile: 'w154', 8 | }; 9 | 10 | export default low; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/medium/low.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const low: MappingImageTypeToImageSize = { 4 | poster: 'w92', 5 | backdrop: 'w300', 6 | still: 'w92', 7 | profile: 'w92', 8 | }; 9 | 10 | export default low; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/small/low.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const low: MappingImageTypeToImageSize = { 4 | poster: 'w92', 5 | backdrop: 'w300', 6 | still: 'w92', 7 | profile: 'w92', 8 | }; 9 | 10 | export default low; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/xsmall/low.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const low: MappingImageTypeToImageSize = { 4 | poster: 'w92', 5 | backdrop: 'w300', 6 | still: 'w92', 7 | profile: 'w92', 8 | }; 9 | 10 | export default low; 11 | -------------------------------------------------------------------------------- /src/styles/border-radius.ts: -------------------------------------------------------------------------------- 1 | import metrics from './metrics'; 2 | 3 | export const borderRadius = { 4 | none: '0', 5 | xs: metrics.xs, 6 | sm: metrics.sm, 7 | md: metrics.md, 8 | lg: metrics.lg, 9 | xl: metrics.xl, 10 | round: metrics.height, 11 | sheet: metrics.lg, 12 | }; 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import 'react-native-gesture-handler'; 5 | import { AppRegistry } from 'react-native'; 6 | import './src/i18n'; 7 | import { App } from './src/App'; 8 | import { name as appName } from './app.json'; 9 | 10 | AppRegistry.registerComponent(appName, () => App); 11 | -------------------------------------------------------------------------------- /src/components/common/images-list/ImagesList.styles.ts: -------------------------------------------------------------------------------- 1 | import { FlatList } from 'react-native'; 2 | import styled from 'styled-components/native'; 3 | 4 | export const Wrapper = styled(FlatList).attrs(({ theme }) => ({ 5 | contentContainerStyle: { 6 | paddingLeft: theme.metrics.md, 7 | }, 8 | }))``; 9 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/participants-list/ParticipantsList.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const List = styled.FlatList.attrs(({ theme }) => ({ 4 | contentContainerStyle: { 5 | paddingLeft: theme.metrics.md, 6 | }, 7 | }))``; 8 | -------------------------------------------------------------------------------- /src/components/stacks/famous/screens/trending-famous/TrendingFamous.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | import metrics from '@styles/metrics'; 4 | 5 | export const sheet = StyleSheet.create({ 6 | contentContainerStyle: { 7 | paddingTop: metrics.md, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/large/high.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const high: MappingImageTypeToImageSize = { 4 | poster: 'w500', 5 | backdrop: 'w780', 6 | still: 'w300', 7 | profile: 'w185', 8 | }; 9 | 10 | export default high; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/medium/high.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const high: MappingImageTypeToImageSize = { 4 | poster: 'w342', 5 | backdrop: 'w780', 6 | still: 'w300', 7 | profile: 'w342', 8 | }; 9 | 10 | export default high; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/small/high.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const high: MappingImageTypeToImageSize = { 4 | poster: 'w342', 5 | backdrop: 'w780', 6 | still: 'w300', 7 | profile: 'w185', 8 | }; 9 | 10 | export default high; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/xlarge/high.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const high: MappingImageTypeToImageSize = { 4 | poster: 'w500', 5 | backdrop: 'w780', 6 | still: 'w300', 7 | profile: 'w342', 8 | }; 9 | 10 | export default high; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/xlarge/low.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const low: MappingImageTypeToImageSize = { 4 | poster: 'w154', 5 | backdrop: 'w300', 6 | still: 'w185', 7 | profile: 'w154', 8 | }; 9 | 10 | export default low; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/xsmall/high.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const high: MappingImageTypeToImageSize = { 4 | poster: 'w185', 5 | backdrop: 'w300', 6 | still: 'w185', 7 | profile: 'w154', 8 | }; 9 | 10 | export default high; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/large/medium.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const medium: MappingImageTypeToImageSize = { 4 | poster: 'w342', 5 | backdrop: 'w300', 6 | still: 'w185', 7 | profile: 'w185', 8 | }; 9 | 10 | export default medium; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/small/medium.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const medium: MappingImageTypeToImageSize = { 4 | poster: 'w154', 5 | backdrop: 'w300', 6 | still: 'w185', 7 | profile: 'w154', 8 | }; 9 | 10 | export default medium; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/xsmall/medium.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const medium: MappingImageTypeToImageSize = { 4 | poster: 'w154', 5 | backdrop: 'w300', 6 | still: 'w92', 7 | profile: 'w92', 8 | }; 9 | 10 | export default medium; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/medium/medium.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const medium: MappingImageTypeToImageSize = { 4 | poster: 'w185', 5 | backdrop: 'w300', 6 | still: 'w185', 7 | profile: 'w154', 8 | }; 9 | 10 | export default medium; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/xlarge/medium.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const medium: MappingImageTypeToImageSize = { 4 | poster: 'w342', 5 | backdrop: 'w300', 6 | still: 'w185', 7 | profile: 'w185', 8 | }; 9 | 10 | export default medium; 11 | -------------------------------------------------------------------------------- /ios/CineTasty.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/common/media-horizontal-list/MediaHorizontalList.style.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | import metrics from '@/styles/metrics'; 4 | 5 | export const sheet = StyleSheet.create({ 6 | flatlist: { 7 | paddingLeft: metrics.md, 8 | marginTop: metrics.sm, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/images-gallery/screen/components/thumbs-list/ThumbsList.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | import metrics from '@styles/metrics'; 4 | 5 | export const sheet = StyleSheet.create({ 6 | list: { 7 | paddingHorizontal: metrics.md, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useTranslation } from './use-translation/use-translation'; 2 | export { useImperativeQuery } from './use-imperative-query/use-imperative-query'; 3 | export { usePagination } from './use-pagination/use-pagination'; 4 | export { useTMDBImageURI } from './use-tmdb-image-uri/use-tmdb-image-uri'; 5 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/xlarge/very-high.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const veryHigh: MappingImageTypeToImageSize = { 4 | poster: 'w780', 5 | backdrop: 'w780', 6 | still: 'w300', 7 | profile: 'w500', 8 | }; 9 | 10 | export default veryHigh; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/xsmall/very-high.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const veryHigh: MappingImageTypeToImageSize = { 4 | poster: 'w342', 5 | backdrop: 'w780', 6 | still: 'w300', 7 | profile: 'w185', 8 | }; 9 | 10 | export default veryHigh; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/large/very-high.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const veryHigh: MappingImageTypeToImageSize = { 4 | poster: 'w780', 5 | backdrop: 'w1280', 6 | still: 'original', 7 | profile: 'w780', 8 | }; 9 | 10 | export default veryHigh; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/medium/very-high.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const veryHigh: MappingImageTypeToImageSize = { 4 | poster: 'w500', 5 | backdrop: 'w1280', 6 | still: 'original', 7 | profile: 'w500', 8 | }; 9 | 10 | export default veryHigh; 11 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/qualities/small/very-high.ts: -------------------------------------------------------------------------------- 1 | import { MappingImageTypeToImageSize } from '../../types'; 2 | 3 | const veryHigh: MappingImageTypeToImageSize = { 4 | poster: 'w500', 5 | backdrop: 'w780', 6 | still: 'original', 7 | profile: 'w342', 8 | }; 9 | 10 | export default veryHigh; 11 | -------------------------------------------------------------------------------- /src/components/common/stars-votes/StarsVotes.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Wrapper = styled.View` 4 | flex-direction: row; 5 | `; 6 | 7 | export const StarsWrapper = styled.View` 8 | flex-direction: row; 9 | margin-left: ${({ theme }) => theme.metrics.xs}px; 10 | `; 11 | -------------------------------------------------------------------------------- /src/utils/format-currency/format-currency.ts: -------------------------------------------------------------------------------- 1 | export const formatCurrency = (value?: number | null) => { 2 | if (!value) { 3 | return '-'; 4 | } 5 | const currencyFormatter = new Intl.NumberFormat('en-US', { 6 | style: 'currency', 7 | currency: 'USD', 8 | }); 9 | return currencyFormatter.format(value); 10 | }; 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); 2 | 3 | /** 4 | * Metro configuration 5 | * https://facebook.github.io/metro/docs/configuration 6 | * 7 | * @type {import('metro-config').MetroConfig} 8 | */ 9 | const config = {}; 10 | 11 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 12 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/biography/Biography.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Wrapper = styled.View` 4 | padding-horizontal: ${({ theme }) => theme.metrics.md}px; 5 | margin-top: ${({ theme }) => theme.metrics.xs}px; 6 | margin-bottom: ${({ theme }) => theme.metrics.lg * 2}px; 7 | `; 8 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/FamousDetails.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const ImagesListWrapper = styled.View` 4 | margin-bottom: ${({ theme }) => theme.metrics.lg * 2}px; 5 | `; 6 | 7 | export const CastGap = styled.View` 8 | width: 1px; 9 | height: ${({ theme }) => theme.metrics.lg * 2}px; 10 | `; 11 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export enum ThemeId { 2 | DARK = 'DARK', 3 | LIGHT = 'LIGHT', 4 | SYSTEM = 'SYSTEM', 5 | } 6 | 7 | export type NewsFilterLanguage = 8 | | 'english' 9 | | 'arabic' 10 | | 'mandarim' 11 | | 'dutch' 12 | | 'french' 13 | | 'german' 14 | | 'hebrew' 15 | | 'italian' 16 | | 'norwegian' 17 | | 'portuguese' 18 | | 'russian' 19 | | 'finnish' 20 | | 'spanish'; 21 | -------------------------------------------------------------------------------- /src/utils/format-date/format-date.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = (language: string, date?: string | null) => { 2 | if (!date) { 3 | return '-'; 4 | } 5 | const [year, month, day] = date.split('-'); 6 | if (!year || !month || !day) { 7 | return '-'; 8 | } 9 | if (language === 'en') { 10 | return `${year}-${month}-${day}`; 11 | } 12 | return `${day}/${month}/${year}`; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/common/section/Section.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { Typography } from '..'; 4 | 5 | export const Wrapper = styled.View` 6 | width: 100%; 7 | `; 8 | 9 | export const Title = styled(Typography.SmallText).attrs({ bold: true })` 10 | margin-bottom: ${({ theme }) => theme.metrics.xs}px; 11 | margin-left: ${({ theme }) => theme.metrics.md}px; 12 | `; 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | globals: { 4 | 'ts-jest': { 5 | tsconfig: 'tsconfig.spec.json', 6 | babelConfig: true, 7 | }, 8 | }, 9 | transformIgnorePatterns: [], 10 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 11 | setupFiles: [ 12 | './jest.setup.js', 13 | './node_modules/react-native-gesture-handler/jestSetup.js', 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | jest.mock('@react-native-async-storage/async-storage', () => 4 | require('@react-native-async-storage/async-storage/jest/async-storage-mock'), 5 | ); 6 | 7 | jest.mock('react-i18next', () => ({ 8 | useTranslation: () => ({ t: key => key, i18n: { language: 'en' } }), 9 | getI18n: () => ({ language: 'en' }), 10 | })); 11 | 12 | require('react-native-reanimated').setUpTests(); 13 | -------------------------------------------------------------------------------- /src/components/common/status-bar/StatusBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StatusBar } from 'react-native'; 3 | 4 | import { useStatusBar } from './use-status-bar'; 5 | 6 | export const StatusBarStyled = () => { 7 | const statusBar = useStatusBar(); 8 | 9 | return ( 10 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /__mocks__/index.ts: -------------------------------------------------------------------------------- 1 | export { RenderHookWrapper, testQuery } from './apollo'; 2 | export * from './utils'; 3 | export { MockedNavigator } from './MockedNavigator'; 4 | export * from './news'; 5 | export * from './quiz-questions'; 6 | export * from './trending-famous'; 7 | export * from './search'; 8 | export * from './famous-details'; 9 | export * from './tv-show-details'; 10 | export * from './tv-show-season-detail'; 11 | export * from './movie-details'; 12 | -------------------------------------------------------------------------------- /src/components/common/paginated-list-header/PaginatedListHeader.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const ReloadButton = styled.TouchableOpacity.attrs(({ theme }) => ({ 4 | hitSlop: { 5 | top: theme.metrics.lg, 6 | bottom: theme.metrics.lg, 7 | left: theme.metrics.lg, 8 | right: theme.metrics.lg, 9 | }, 10 | }))` 11 | align-self: center; 12 | margin-top: ${({ theme }) => theme.metrics.lg}px; 13 | `; 14 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/index.ts: -------------------------------------------------------------------------------- 1 | export { FamousDetails } from './famous-details/FamousDetails'; 2 | export { Search } from './search/Search'; 3 | export { ImagesGallery } from './images-gallery/screen/ImagesGallery'; 4 | export { TVShowDetails } from './media-details/tv-show-details/screen/TVShowDetails'; 5 | export { TVShowSeason } from './tv-show-season/screen/TVShowSeason'; 6 | export { MovieDetails } from './media-details/movie-details/screen/MovieDetails'; 7 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/components/search-bar/use-search-bar.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { TextInput } from 'react-native'; 3 | 4 | export const useSearchBar = () => { 5 | const inputRef = useRef(null); 6 | 7 | useEffect(() => { 8 | if (inputRef && inputRef.current) { 9 | inputRef.current.focus(); 10 | } 11 | }, []); 12 | 13 | return { 14 | inputRef, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/common/section/Section.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | import * as Styles from './Section.styles'; 4 | 5 | export type SectionProps = { 6 | children: ReactNode; 7 | title: string; 8 | }; 9 | 10 | export const Section = (props: SectionProps) => ( 11 | 12 | {props.title} 13 | {props.children} 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/components/common/status-bar/use-status-bar.ts: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'styled-components/native'; 2 | import { StatusBarStyle } from 'react-native'; 3 | 4 | import { dark } from '@styles/themes'; 5 | 6 | export const useStatusBar = () => { 7 | const theme = useTheme(); 8 | 9 | return { 10 | barStyle: (theme.id === dark.id 11 | ? 'light-content' 12 | : 'dark-content') as StatusBarStyle, 13 | backgroundColor: theme.colors.secondary, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/news-list-item/date-diff/DateDiff.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { Typography } from '@/components/common'; 4 | import { ThemeId } from '@/types'; 5 | 6 | export const DefaultText = styled(Typography.ExtraSmallText).attrs( 7 | ({ theme }) => ({ 8 | color: 9 | theme.id === ThemeId.DARK 10 | ? theme.colors.subText 11 | : theme.colors.buttonText, 12 | }), 13 | )``; 14 | -------------------------------------------------------------------------------- /src/components/stacks/news/routes/route-params-types.ts: -------------------------------------------------------------------------------- 1 | import { StackNavigationProp } from '@react-navigation/stack'; 2 | 3 | import { Routes } from '@navigation'; 4 | 5 | type NewsStackParams = { 6 | [Routes.News.NEWS]: undefined; 7 | }; 8 | 9 | /** News-Stack-Props */ 10 | export type NewsStackNavigationProp = StackNavigationProp< 11 | NewsStackParams, 12 | Routes.News.NEWS 13 | >; 14 | 15 | export type NewsStackProps = { 16 | navigation: NewsStackNavigationProp; 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/use-setup-questions/types.ts: -------------------------------------------------------------------------------- 1 | import { Translations } from '@/i18n/tags'; 2 | import { 3 | QuizQuestionCategory, 4 | QuizQuestionDifficulty, 5 | QuizQuestionType, 6 | } from '@schema-types'; 7 | 8 | export type OptionValue = 9 | | QuizQuestionCategory 10 | | QuizQuestionDifficulty 11 | | QuizQuestionType; 12 | 13 | export type SetupQuestionOption = { 14 | translationTag: Translations.Quiz; 15 | value: OptionValue; 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/news-loading/NewsLoading.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import styled from 'styled-components/native'; 3 | 4 | import metrics from '@styles/metrics'; 5 | 6 | export const LoadingList = styled.View` 7 | flex: 1; 8 | `; 9 | 10 | export const sheet = StyleSheet.create({ 11 | placeholder: { 12 | width: '100%', 13 | height: metrics.getWidthFromDP('8'), 14 | borderRadius: metrics.xs, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { storage } from './storage/storage'; 2 | export { renderSVGIconConditionally } from './render-svg-icon-conditionally/render-svg-icon-conditionally'; 3 | export { isEqualsOrLargerThanIphoneX } from './is-equals-or-larger-than-iphonex/is-equals-or-larger-than-iphonex'; 4 | export { getStatusBarHeight } from './status-bar-height/get-statusbar-height'; 5 | export { formatDate } from './format-date/format-date'; 6 | export { formatCurrency } from './format-currency/format-currency'; 7 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/images-gallery/screen/components/images-list/ImagesList.styles.ts: -------------------------------------------------------------------------------- 1 | import metrics from '@/styles/metrics'; 2 | import { StyleSheet } from 'react-native'; 3 | import styled from 'styled-components/native'; 4 | 5 | export const PlaceholderListItem = styled.View` 6 | width: ${({ theme }) => theme.metrics.width}px; 7 | height: 100%; 8 | `; 9 | 10 | export const sheet = StyleSheet.create({ 11 | flatlist: { 12 | marginTop: metrics.getHeightFromDP('20'), 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/News.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { WRAPPER_HEIGHT } from '@/navigation/components/tab-navigator/TabNavigator.styles'; 4 | import metrics from '@styles/metrics'; 5 | 6 | import { imageWrapper } from './components/news-list-item/NewsListItem.styles'; 7 | 8 | export const LIST_ITEM_HEIGHT = imageWrapper.height + 2 * metrics.md; 9 | 10 | export const Container = styled.View` 11 | flex: 1; 12 | padding-bottom: ${WRAPPER_HEIGHT}px; 13 | `; 14 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/videos/use-videos.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Linking } from 'react-native'; 3 | 4 | export const YOUTUBE_BASE_URL = 'https://www.youtube.com/watch?v='; 5 | 6 | export const useVideos = () => { 7 | const handlePress = useCallback((key?: string | null) => { 8 | if (!key) { 9 | return; 10 | } 11 | Linking.openURL(`${YOUTUBE_BASE_URL}${key}`); 12 | }, []); 13 | 14 | return { 15 | onPress: handlePress, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/components/recent-searches/RecentSearches.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { Typography } from '@/components/common'; 4 | 5 | export const Wrapper = styled.View` 6 | flex-direction: column; 7 | padding-top: ${({ theme }) => theme.metrics.xl}px; 8 | padding-horizontal: ${({ theme }) => theme.metrics.lg}px; 9 | `; 10 | 11 | export const RecentText = styled(Typography.ExtraSmallText)` 12 | margin-bottom: ${({ theme }) => theme.metrics.xl}px; 13 | `; 14 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/debounce.ts: -------------------------------------------------------------------------------- 1 | export const debounce = any>( 2 | timeoutCallback: T, 3 | delay: number, 4 | ) => { 5 | let timeout: ReturnType | null = null; 6 | const debounced = (...args: Parameters) => { 7 | if (timeout !== null) { 8 | clearTimeout(timeout); 9 | timeout = null; 10 | } 11 | timeout = setTimeout(() => timeoutCallback(...args), delay); 12 | }; 13 | 14 | return debounced as (...args: Parameters) => ReturnType; 15 | }; 16 | -------------------------------------------------------------------------------- /src/navigation/components/AndroidNavigationBar.android.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import changeNavigationBarColor from 'react-native-navigation-bar-color'; 3 | import { useTheme } from 'styled-components/native'; 4 | 5 | import { ThemeId } from '@app-types'; 6 | 7 | export const AndroidNavigationBar = () => { 8 | const theme = useTheme(); 9 | 10 | useEffect(() => { 11 | const isLight = ThemeId.LIGHT === theme.id; 12 | changeNavigationBarColor(theme.colors.secondary, isLight, true); 13 | }, [theme]); 14 | 15 | return <>; 16 | }; 17 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | ThemeContextProvider, 5 | GraphQLClient, 6 | AlertMessageProvider, 7 | TMDBImageQualitiesProvider, 8 | } from '@providers'; 9 | import { Navigation } from '@navigation'; 10 | 11 | export const App = () => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /src/navigation/navigation-utils.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components/native'; 2 | 3 | export const defaultHeaderStyle = { 4 | headerBackTitleVisible: false, 5 | headerStyle: { 6 | shadowColor: 'transparent', 7 | elevation: 0, 8 | }, 9 | }; 10 | 11 | export const getTransparentHeaderOptions = (theme: DefaultTheme) => ({ 12 | headerBackTitleVisible: false, 13 | headerTransparent: true, 14 | headerTintColor: theme.colors.text, 15 | title: '', 16 | headerStyle: { 17 | shadowColor: 'transparent', 18 | elevation: 0, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/news-list-item/date-diff/DateDiff.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as Styles from './DateDiff.styles'; 4 | import { useDateDiff } from './use-data-diff'; 5 | 6 | type DateDiffProps = { 7 | date: string; 8 | now: Date; 9 | }; 10 | 11 | export const DateDiff = (props: DateDiffProps) => { 12 | const dateDiff = useDateDiff({ date: props.date, now: props.now }); 13 | 14 | return ( 15 | 16 | {dateDiff.text} 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/use-setup-questions/options/types.ts: -------------------------------------------------------------------------------- 1 | import { Translations } from '@/i18n/tags'; 2 | import { QuizQuestionType } from '@schema-types'; 3 | 4 | export const types = [ 5 | { 6 | value: QuizQuestionType.MIXED, 7 | translationTag: Translations.Quiz.QUIZ_TYPE_MIXED, 8 | }, 9 | { 10 | value: QuizQuestionType.MULTIPLE, 11 | translationTag: Translations.Quiz.QUIZ_TYPE_MULTIPLE, 12 | }, 13 | { 14 | value: QuizQuestionType.BOOLEAN, 15 | translationTag: Translations.Quiz.QUIZ_TYPE_BOOLEAN, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /src/components/common/rounded-button/RoundedButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as Styles from './RoundedButton.styles'; 4 | 5 | type RoundedButtonProps = { 6 | isDisabled?: boolean; 7 | onPress: () => void; 8 | text: string; 9 | }; 10 | 11 | export const RoundedButton = (props: RoundedButtonProps) => ( 12 | 16 | 17 | {props.text} 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/biography/Biography.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { MediaItemDescription, Section } from '@common-components'; 4 | 5 | import * as Styles from './Biography.styles'; 6 | 7 | type BiographyProps = { 8 | sectionTitle: string; 9 | text: string; 10 | }; 11 | 12 | export const Biography = (props: BiographyProps) => ( 13 |
14 | 15 | 16 | 17 |
18 | ); 19 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/use-setup-questions/options/categories.ts: -------------------------------------------------------------------------------- 1 | import { QuizQuestionCategory } from '@schema-types'; 2 | import { Translations } from '@/i18n/tags'; 3 | 4 | export const categories = [ 5 | { 6 | value: QuizQuestionCategory.MIXED, 7 | translationTag: Translations.Quiz.QUIZ_CATEGORY_MIXED, 8 | }, 9 | { 10 | value: QuizQuestionCategory.MOVIE, 11 | translationTag: Translations.Quiz.QUIZ_CATEGORY_MOVIE, 12 | }, 13 | { 14 | value: QuizQuestionCategory.TV, 15 | translationTag: Translations.Quiz.QUIZ_CATEGORY_TV, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/common/paginated-list-header/PaginatedListHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import metrics from '@/styles/metrics'; 4 | 5 | import * as Styles from './PaginatedListHeader.styles'; 6 | import { SVGIcon } from '../svg-icon/SVGIcon'; 7 | 8 | type PaginatedListHeaderProps = { 9 | onPress: () => void; 10 | }; 11 | 12 | export const PaginatedListHeader = (props: PaginatedListHeaderProps) => ( 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/components/common/scroll-with-animated-header/use-scroll-with-animated-header.ts: -------------------------------------------------------------------------------- 1 | import { NativeScrollEvent } from 'react-native'; 2 | import { 3 | useAnimatedScrollHandler, 4 | useSharedValue, 5 | } from 'react-native-reanimated'; 6 | 7 | export const useScrollWithAnimatedHeader = () => { 8 | const scrollViewPosition = useSharedValue(0); 9 | 10 | const handleScroll = useAnimatedScrollHandler((event: NativeScrollEvent) => { 11 | scrollViewPosition.value = event.contentOffset.y; 12 | }); 13 | 14 | return { 15 | onScroll: handleScroll, 16 | scrollViewPosition, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/media-info/MediaInfo.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Wrapper = styled.View` 4 | width: 100%; 5 | flex-direction: row; 6 | flex-wrap: wrap; 7 | justify-content: center; 8 | padding-vertical: ${({ theme }) => theme.metrics.sm}px; 9 | background-color: ${({ theme }) => theme.colors.secondary}; 10 | `; 11 | 12 | export const InfoCellWrapper = styled.View` 13 | width: 46%; 14 | margin-vertical: ${({ theme }) => theme.metrics.sm}px; 15 | margin-horizontal: ${({ theme }) => theme.metrics.xs}px; 16 | `; 17 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/components/number-of-questions/NumberOfQuestionts.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import Slider from '@react-native-community/slider'; 3 | 4 | export const NumberQuestionsWrapper = styled.View` 5 | width: 100%; 6 | flex-direction: row; 7 | justify-content: space-between; 8 | align-items: center; 9 | `; 10 | 11 | export const SliderStyled = styled(Slider).attrs(({ theme }) => ({ 12 | maximumTrackTintColor: theme.colors.contrast, 13 | minimumTrackTintColor: theme.colors.text, 14 | thumbTintColor: theme.colors.text, 15 | }))``; 16 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/loading/MediaDetailsLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { LoadingPlaceholder } from '@common-components'; 4 | 5 | import * as Styles from './MediaDetails.styles'; 6 | 7 | export const MediaDetailsLoading = () => ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /ios/link-assets-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "migIndex": 1, 3 | "data": [ 4 | { 5 | "path": "assets/fonts/CircularStd-Black.otf", 6 | "sha1": "c3151c999f79358f3a091222ebefcd4d436cbe8e" 7 | }, 8 | { 9 | "path": "assets/fonts/CircularStd-Bold.otf", 10 | "sha1": "2a36de6df85e4da5c22d26a427fd7390ad32840f" 11 | }, 12 | { 13 | "path": "assets/fonts/CircularStd-Book.otf", 14 | "sha1": "a67856765459bf64a0a1259b6621613edd717ff9" 15 | }, 16 | { 17 | "path": "assets/fonts/CircularStd-Medium.otf", 18 | "sha1": "29b0a9af2e480ffb9d45de38a44f1c21a002adc9" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /android/link-assets-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "migIndex": 1, 3 | "data": [ 4 | { 5 | "path": "assets/fonts/CircularStd-Black.otf", 6 | "sha1": "c3151c999f79358f3a091222ebefcd4d436cbe8e" 7 | }, 8 | { 9 | "path": "assets/fonts/CircularStd-Bold.otf", 10 | "sha1": "2a36de6df85e4da5c22d26a427fd7390ad32840f" 11 | }, 12 | { 13 | "path": "assets/fonts/CircularStd-Book.otf", 14 | "sha1": "a67856765459bf64a0a1259b6621613edd717ff9" 15 | }, 16 | { 17 | "path": "assets/fonts/CircularStd-Medium.otf", 18 | "sha1": "29b0a9af2e480ffb9d45de38a44f1c21a002adc9" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /__mocks__/apollo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { gql } from '@apollo/client'; 3 | 4 | import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; 5 | 6 | type RenderHookWrapperProps = { children: React.ReactNode }; 7 | 8 | export const RenderHookWrapper = (props: RenderHookWrapperProps) => ( 9 | 15 | {props.children} 16 | 17 | ); 18 | 19 | export const testQuery = gql` 20 | query TestQuery { 21 | testQuery { 22 | someField 23 | } 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/images-gallery/screen/components/images-list/images-list-item/ImagesListItem.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import metrics from '@styles/metrics'; 4 | 5 | export const LANDSCAPE_HEIGHT = metrics.getWidthFromDP('65'); 6 | export const PORTRAIT_HEIGHT = metrics.getWidthFromDP('90'); 7 | 8 | export const Wrapper = styled.View` 9 | width: ${({ theme }) => theme.metrics.width}px; 10 | height: ${PORTRAIT_HEIGHT}px; 11 | `; 12 | 13 | export const ImageWrapper = styled.View` 14 | height: 100%; 15 | width: 100%; 16 | justify-content: center; 17 | align-items: center; 18 | `; 19 | -------------------------------------------------------------------------------- /src/providers/tmdb-image-qualities/types.ts: -------------------------------------------------------------------------------- 1 | export type ImageQualities = 'low' | 'medium' | 'high' | 'veryHigh'; 2 | 3 | export type DeviceScreenClassification = 4 | | 'xsmall' 5 | | 'small' 6 | | 'medium' 7 | | 'large' 8 | | 'xlarge'; 9 | 10 | export type ImageType = 'backdrop' | 'poster' | 'still' | 'profile'; 11 | 12 | export type MappingImageTypeToImageSize = { 13 | poster: 'w92' | 'w154' | 'w185' | 'w342' | 'w500' | 'w780' | 'orginal'; 14 | backdrop: 'w300' | 'w780' | 'w1280' | 'original'; 15 | still: 'w92' | 'w185' | 'w300' | 'original'; 16 | profile: 'w92' | 'w154' | 'w185' | 'w342' | 'w500' | 'w780' | 'original'; 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/format-currency/format-currency.test.ts: -------------------------------------------------------------------------------- 1 | import { formatCurrency } from './format-currency'; 2 | 3 | describe('Utils/format-date', () => { 4 | it('should return correctly when "value" is "undefined"', () => { 5 | const result = formatCurrency(); 6 | expect(result).toEqual('-'); 7 | }); 8 | 9 | it('should return correctly when "value" is "null"', () => { 10 | const result = formatCurrency(null); 11 | expect(result).toEqual('-'); 12 | }); 13 | 14 | it('should return correctly when "value" is "defined"', () => { 15 | const result = formatCurrency(10); 16 | expect(result).toEqual('$10.00'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/news-list-item/use-news-list-item.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Linking } from 'react-native'; 3 | 4 | type UseNewsListItemParams = { 5 | url?: string; 6 | }; 7 | 8 | export const useNewsListItem = (params: UseNewsListItemParams) => { 9 | const handlePress = useCallback(async () => { 10 | if (!params.url) { 11 | return; 12 | } 13 | const canOpenURL = await Linking.canOpenURL(params.url); 14 | if (canOpenURL) { 15 | Linking.openURL(params.url); 16 | } 17 | }, [params.url]); 18 | 19 | return { 20 | onPress: handlePress, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/images-gallery/routes/route-params-types.ts: -------------------------------------------------------------------------------- 1 | import { StackScreenProps } from '@react-navigation/stack'; 2 | 3 | import { FamousStackRoutes } from '@/components/stacks/famous/routes/route-params-types'; 4 | import { HomeStackRoutes } from '@/components/stacks/home/routes/route-params-types'; 5 | import { Routes } from '@/navigation'; 6 | 7 | export type ImagesGalleryProps = StackScreenProps< 8 | FamousStackRoutes & HomeStackRoutes, 9 | Routes.Home.IMAGES_GALLERY | Routes.Famous.IMAGES_GALLERY 10 | >; 11 | 12 | export type ImagesGalleryNavigationProps = { 13 | indexSelected: number; 14 | images: string[]; 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/results/Results.styles.ts: -------------------------------------------------------------------------------- 1 | import metrics from '@/styles/metrics'; 2 | import { StyleSheet } from 'react-native'; 3 | import styled from 'styled-components/native'; 4 | 5 | export const Wrapper = styled.View` 6 | width: 100%; 7 | height: 100%; 8 | align-items: center; 9 | `; 10 | 11 | export const PlayAgainButtonWrapper = styled.View` 12 | position: absolute; 13 | bottom: ${({ theme }) => theme.metrics.lg * 2}px; 14 | `; 15 | 16 | export const sheet = StyleSheet.create({ 17 | list: { 18 | paddingBottom: metrics.lg * 4, 19 | paddingHorizontal: metrics.lg, 20 | paddingTop: metrics.lg, 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /src/utils/status-bar-height/get-statusbar-height.ts: -------------------------------------------------------------------------------- 1 | import { Platform, StatusBar } from 'react-native'; 2 | 3 | import { isEqualsOrLargerThanIphoneX } from '../is-equals-or-larger-than-iphonex/is-equals-or-larger-than-iphonex'; 4 | 5 | export const IOS_IPHONE_X_AND_ABOVE = 44; 6 | export const IOS_BELOW_IPHONE_X = 20; 7 | 8 | export const getStatusBarHeight = () => { 9 | if (isEqualsOrLargerThanIphoneX()) { 10 | return IOS_IPHONE_X_AND_ABOVE; 11 | } 12 | if (Platform.OS === 'ios') { 13 | return IOS_BELOW_IPHONE_X; 14 | } 15 | if (Platform.OS === 'android') { 16 | return StatusBar.currentHeight || 0; 17 | } 18 | return 0; 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/SetupQuestions.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Wrapper = styled.View` 4 | width: 100%; 5 | height: 100%; 6 | justify-content: space-between; 7 | padding-horizontal: ${({ theme }) => theme.metrics.md}px; 8 | padding-bottom: ${({ theme }) => theme.metrics.getWidthFromDP('10')}px; 9 | `; 10 | 11 | export const RoundedButtonWrapper = styled.View` 12 | align-items: center; 13 | `; 14 | 15 | export const SectionWrapper = styled.View.attrs(() => ({ 16 | testID: 'section-wrapper', 17 | }))` 18 | margin-top: ${({ theme }) => theme.metrics.getWidthFromDP('8')}px; 19 | `; 20 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/tv-show-season/routes/route-params-types.ts: -------------------------------------------------------------------------------- 1 | import { StackScreenProps } from '@react-navigation/stack'; 2 | 3 | import { FamousStackRoutes } from '@/components/stacks/famous/routes/route-params-types'; 4 | import { HomeStackRoutes } from '@/components/stacks/home/routes/route-params-types'; 5 | import { Routes } from '@/navigation'; 6 | 7 | export type TVShowSeasonProps = StackScreenProps< 8 | FamousStackRoutes & HomeStackRoutes, 9 | Routes.Home.TV_SHOW_SEASON | Routes.Famous.TV_SHOW_SEASON 10 | >; 11 | 12 | export type TVShowSeasonNavigationProps = { 13 | season: number; 14 | id?: number | null; 15 | name?: string | null; 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/routes/route-params-types.ts: -------------------------------------------------------------------------------- 1 | import { StackScreenProps } from '@react-navigation/stack'; 2 | 3 | import { FamousStackRoutes } from '@/components/stacks/famous/routes/route-params-types'; 4 | import { HomeStackRoutes } from '@/components/stacks/home/routes/route-params-types'; 5 | import { Routes } from '@/navigation'; 6 | 7 | export type FamousDetailsNavigationProps = StackScreenProps< 8 | FamousStackRoutes & HomeStackRoutes, 9 | Routes.Famous.DETAILS | Routes.Home.FAMOUS_DETAILS 10 | >; 11 | 12 | export type FamousDetailsProps = { 13 | profileImage?: string | null; 14 | name?: string | null; 15 | id?: number | null; 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils/is-equals-or-larger-than-iphonex/is-equals-or-larger-than-iphonex.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions, Platform } from 'react-native'; 2 | 3 | const { width, height } = Dimensions.get('window'); 4 | 5 | const IPHONEX_WIDTH = 375; 6 | const IPHONEX_HEIGHT = 812; 7 | 8 | export const isEqualsOrLargerThanIphoneX = () => { 9 | const isLargestThanIphoneXInPortraitMode = 10 | height >= IPHONEX_HEIGHT && width >= IPHONEX_WIDTH; 11 | const isLargestThanIphoneXInLandscapeMode = 12 | height >= IPHONEX_WIDTH && width >= IPHONEX_HEIGHT; 13 | return ( 14 | Platform.OS === 'ios' && 15 | (isLargestThanIphoneXInPortraitMode || isLargestThanIphoneXInLandscapeMode) 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "33.0.0" 6 | minSdkVersion = 21 7 | compileSdkVersion = 33 8 | targetSdkVersion = 33 9 | 10 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. 11 | ndkVersion = "23.1.7779620" 12 | } 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | dependencies { 18 | classpath("com.android.tools.build:gradle") 19 | classpath("com.facebook.react:react-native-gradle-plugin") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/i18n/language-detection.ts: -------------------------------------------------------------------------------- 1 | import { Platform, NativeModules } from 'react-native'; 2 | 3 | const detectDeviceLanguage = () => 4 | Platform.OS === 'ios' 5 | ? NativeModules.SettingsManager.settings.AppleLocale || 6 | NativeModules.SettingsManager.settings.AppleLanguages[0] 7 | : NativeModules.I18nManager.localeIdentifier; 8 | 9 | export const languageDetection = ( 10 | fallbackLanguage: string, 11 | supportedLanguages: string[], 12 | ) => { 13 | const deviceLanguage = detectDeviceLanguage() as string; 14 | const language = deviceLanguage.slice(0, 2); 15 | const isSupportedLanguage = supportedLanguages.indexOf(language) >= 0; 16 | return isSupportedLanguage ? language : fallbackLanguage; 17 | }; 18 | -------------------------------------------------------------------------------- /src/styles/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions, Platform, PixelRatio } from 'react-native'; 2 | 3 | const { width, height } = Dimensions.get('window'); 4 | 5 | const getWidthFromDP = (dp: string) => 6 | PixelRatio.roundToNearestPixel((width * parseFloat(`${dp}%`)) / 100); 7 | 8 | const getHeightFromDP = (dp: string) => 9 | PixelRatio.roundToNearestPixel((height * parseFloat(`${dp}%`)) / 100); 10 | 11 | export default { 12 | navigationHeaderFontSize: Platform.OS === 'ios' ? 17 : 19, 13 | xs: getWidthFromDP('1'), 14 | sm: getWidthFromDP('2'), 15 | md: getWidthFromDP('3'), 16 | lg: getWidthFromDP('4'), 17 | xl: getWidthFromDP('5'), 18 | getWidthFromDP, 19 | getHeightFromDP, 20 | width, 21 | height, 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/header/birthday-text/BirthDayText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Typography } from '@/components/common'; 4 | 5 | import { useBirthDayText } from './useBirthDayText'; 6 | 7 | type BirthdayTextProps = { 8 | rawBirthDate?: string | null; 9 | }; 10 | 11 | export const BirthDayText = (props: BirthdayTextProps) => { 12 | const birthDayText = useBirthDayText({ 13 | rawDateString: props.rawBirthDate ?? '-', 14 | }); 15 | 16 | if (!props.rawBirthDate) { 17 | return; 18 | } 19 | 20 | return ( 21 | 22 | {birthDayText.text} 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/tv-show-details/screen/components/seasons-section/SeasonsSection.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const SeasonWrapper = styled.TouchableOpacity` 4 | align-self: flex-start; 5 | margin-right: ${({ theme }) => theme.metrics.lg}px; 6 | padding: ${({ theme }) => theme.metrics.md}px; 7 | background-color: ${({ theme }) => theme.colors.primary}; 8 | border-radius: ${({ theme }) => theme.borderRadius.sm}px; 9 | `; 10 | 11 | export const List = styled.ScrollView.attrs(({ theme }) => ({ 12 | showsHorizontalScrollIndicator: false, 13 | contentContainerStyle: { 14 | paddingLeft: theme.metrics.md, 15 | }, 16 | horizontal: true, 17 | }))``; 18 | -------------------------------------------------------------------------------- /src/components/common/default-tmdb-list-loading/DefaultTMDBListLoading.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { 4 | DEFAULT_MARGIN_LEFT, 5 | DEFAULT_HEIGHT, 6 | } from '../default-tmdb-list-item/DefaultTMDBListItem.styles'; 7 | import metrics from '@/styles/metrics'; 8 | 9 | const NUMBER_OF_COLUMNS = 3; 10 | export const NUMBER_OF_LOADING_ITEMS = Math.ceil( 11 | NUMBER_OF_COLUMNS * (metrics.height / DEFAULT_HEIGHT) + 1, 12 | ); 13 | 14 | export const LoadingWrapper = styled.View` 15 | flex: 1; 16 | flex-direction: row; 17 | flex-wrap: wrap; 18 | justify-content: space-between; 19 | padding-top: ${({ theme }) => theme.metrics.md}px; 20 | padding-horizontal: ${DEFAULT_MARGIN_LEFT}px; 21 | `; 22 | -------------------------------------------------------------------------------- /src/components/common/rounded-button/RoundedButton.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { Typography } from '..'; 4 | 5 | export const Wrapper = styled.TouchableOpacity` 6 | padding-horizontal: ${({ theme }) => theme.metrics.getWidthFromDP('10%')}px; 7 | padding-vertical: ${({ theme }) => theme.metrics.lg}px; 8 | background-color: ${({ theme }) => theme.colors.primary}; 9 | border-radius: ${({ theme }) => theme.metrics.width}px; 10 | opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; 11 | `; 12 | 13 | export const ButtonText = styled(Typography.SmallText).attrs(({ theme }) => ({ 14 | color: theme.colors.buttonText, 15 | alignment: 'center', 16 | }))` 17 | text-transform: uppercase; 18 | `; 19 | -------------------------------------------------------------------------------- /src/providers/graphql-client/GraphQLClient.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { 3 | ApolloClient, 4 | InMemoryCache, 5 | ApolloProvider, 6 | HttpLink, 7 | ApolloLink, 8 | } from '@apollo/client'; 9 | 10 | type GraphQLClientProps = { 11 | children: React.ReactNode; 12 | }; 13 | 14 | export const GraphQLClient = (props: GraphQLClientProps) => { 15 | const client = useMemo(() => { 16 | const httpLink = new HttpLink({ 17 | uri: process.env.SERVER_URL, 18 | }); 19 | return new ApolloClient({ 20 | link: ApolloLink.from([httpLink]), 21 | cache: new InMemoryCache(), 22 | }); 23 | }, []); 24 | 25 | return {props.children}; 26 | }; 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native', 4 | rules: { 5 | 'no-console': 1, 6 | 'no-var': 'error', 7 | semi: 'error', 8 | indent: ['error', 2, { SwitchCase: 1 }], 9 | 'no-multi-spaces': 'error', 10 | 'space-in-parens': 'error', 11 | 'no-multiple-empty-lines': 'error', 12 | 'prefer-const': 'error', 13 | 'object-curly-spacing': ['error', 'always', { objectsInObjects: false }], 14 | '@typescript-eslint/no-unused-vars': [ 15 | 'warn', // or "error" 16 | { 17 | argsIgnorePattern: '^_', 18 | varsIgnorePattern: '^_', 19 | caughtErrorsIgnorePattern: '^_', 20 | }, 21 | ], 22 | 'react-hooks/exhaustive-deps': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/use-setup-questions/options/difficulties.ts: -------------------------------------------------------------------------------- 1 | import { QuizQuestionDifficulty } from '@schema-types'; 2 | import { Translations } from '@/i18n/tags'; 3 | 4 | export const difficulties = [ 5 | { 6 | value: QuizQuestionDifficulty.MIXED, 7 | translationTag: Translations.Quiz.QUIZ_DIFFICULTY_MIXED, 8 | }, 9 | { 10 | value: QuizQuestionDifficulty.EASY, 11 | translationTag: Translations.Quiz.QUIZ_DIFFICULTY_EASY, 12 | }, 13 | { 14 | value: QuizQuestionDifficulty.MEDIUM, 15 | translationTag: Translations.Quiz.QUIZ_DIFFICULTY_MEDIUM, 16 | }, 17 | { 18 | value: QuizQuestionDifficulty.HARD, 19 | translationTag: Translations.Quiz.QUIZ_DIFFICULTY_HARD, 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/header/header-loading-placeholder/HeaderLoadingPlaceholder.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet, View } from 'react-native'; 2 | import styled from 'styled-components/native'; 3 | 4 | import metrics from '@styles/metrics'; 5 | 6 | const DEFAULT_STYLE = { 7 | width: metrics.getWidthFromDP('60'), 8 | height: metrics.lg, 9 | borderRadius: metrics.height, 10 | }; 11 | 12 | export const LoadingWrapper = styled(View)` 13 | margin-left: ${({ theme }) => theme.metrics.md}px; 14 | flex-direction: row; 15 | align-items: center; 16 | `; 17 | 18 | export const styles = StyleSheet.create({ 19 | loadingItem: DEFAULT_STYLE, 20 | middleItem: { 21 | ...DEFAULT_STYLE, 22 | marginVertical: metrics.sm, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/navigation/components/tab-navigator/TabNavigator.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import Animated from 'react-native-reanimated'; 3 | 4 | import { isEqualsOrLargerThanIphoneX } from '@utils'; 5 | import metrics from '@styles/metrics'; 6 | 7 | export const WRAPPER_HEIGHT = 8 | metrics.getWidthFromDP('18') + 9 | (isEqualsOrLargerThanIphoneX() ? metrics.getWidthFromDP('8') : 0); 10 | 11 | export const Wrapper = styled(Animated.View)` 12 | width: ${({ theme }) => theme.metrics.width}px; 13 | height: ${WRAPPER_HEIGHT}px; 14 | position: absolute; 15 | flex-direction: row; 16 | background-color: ${({ theme }) => theme.colors.secondary}; 17 | padding-bottom: ${isEqualsOrLargerThanIphoneX() ? 30 : 0}px; 18 | bottom: 0px; 19 | right: 0px; 20 | `; 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master, development] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [18.x] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Installing Dependencies 23 | run: npm install 24 | 25 | - name: Running prettier 26 | run: npm run prettier:fix 27 | 28 | - name: Running eslint 29 | run: npm run lint:fix 30 | 31 | - name: Running tests 32 | run: npm run test 33 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/movie-details/routes/route-params-types.ts: -------------------------------------------------------------------------------- 1 | import { StackScreenProps } from '@react-navigation/stack'; 2 | 3 | import { FamousStackRoutes } from '@/components/stacks/famous/routes/route-params-types'; 4 | import { HomeStackRoutes } from '@/components/stacks/home/routes/route-params-types'; 5 | import { Routes } from '@/navigation'; 6 | 7 | export type MovieDetailsProps = StackScreenProps< 8 | FamousStackRoutes & HomeStackRoutes, 9 | Routes.Home.MOVIE_DETAILS | Routes.Famous.MOVIE_DETAILS 10 | >; 11 | 12 | export type MovieDetailsNavigationProps = { 13 | voteAverage?: number | null; 14 | genres?: string[] | null; 15 | voteCount?: number | null; 16 | image?: string | null; 17 | title?: string | null; 18 | id?: number | null; 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/tv-show-details/routes/route-params-types.ts: -------------------------------------------------------------------------------- 1 | import { StackScreenProps } from '@react-navigation/stack'; 2 | 3 | import { FamousStackRoutes } from '@/components/stacks/famous/routes/route-params-types'; 4 | import { HomeStackRoutes } from '@/components/stacks/home/routes/route-params-types'; 5 | import { Routes } from '@/navigation'; 6 | 7 | export type TVShowDetailsProps = StackScreenProps< 8 | FamousStackRoutes & HomeStackRoutes, 9 | Routes.Home.TV_SHOW_DETAILS | Routes.Famous.TV_SHOW_DETAILS 10 | >; 11 | 12 | export type TVShowDetailsNavigationProps = { 13 | voteAverage?: number | null; 14 | genres?: string[] | null; 15 | voteCount?: number | null; 16 | image?: string | null; 17 | title?: string | null; 18 | id?: number | null; 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/common/paginated-list-footer/PaginatedListFooter.styles.ts: -------------------------------------------------------------------------------- 1 | import { ActivityIndicator, TouchableOpacity, View } from 'react-native'; 2 | import styled from 'styled-components/native'; 3 | 4 | export const Wrapper = styled(View)` 5 | width: 100%; 6 | height: ${({ theme }) => theme.metrics.getWidthFromDP('18%')}px; 7 | justify-content: center; 8 | align-items: center; 9 | `; 10 | 11 | export const LoadButton = styled(TouchableOpacity).attrs(({ theme }) => ({ 12 | hitSlop: { 13 | top: theme.metrics.lg, 14 | bottom: theme.metrics.lg, 15 | left: theme.metrics.lg, 16 | right: theme.metrics.lg, 17 | }, 18 | }))``; 19 | 20 | export const CustomActivityIndicator = styled(ActivityIndicator).attrs( 21 | ({ theme }) => ({ 22 | color: theme.colors.text, 23 | }), 24 | )``; 25 | -------------------------------------------------------------------------------- /android/app/src/release/java/com/cinetasty/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.cinetasty; 8 | 9 | import android.content.Context; 10 | import com.facebook.react.ReactInstanceManager; 11 | 12 | /** 13 | * Class responsible of loading Flipper inside your React Native application. This is the release 14 | * flavor of it so it's empty as we don't want to load Flipper. 15 | */ 16 | public class ReactNativeFlipper { 17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 18 | // Do nothing as we don't want to initialize Flipper on Release. 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CineTastyTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/common/modal-select-button/ModalSelectButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as Styles from './ModalSelectButton.styles'; 4 | 5 | export type ModalSelectButtonProps = { 6 | borderBottomRightRadius?: number; 7 | borderBottomLeftRadius?: number; 8 | isDisabled?: boolean; 9 | onPress: () => void; 10 | title: string; 11 | }; 12 | 13 | export const ModalSelectButton = (props: ModalSelectButtonProps) => ( 14 | 20 | 21 | {props.title} 22 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/death-day/DeathDay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SVGIcon } from '@common-components'; 4 | import metrics from '@styles/metrics'; 5 | 6 | import * as Styles from './DeathDay.styles'; 7 | import { useDeathDay } from './use-death-day'; 8 | 9 | type DeathDayProps = { 10 | day: string; 11 | }; 12 | 13 | export const DeathDay = (props: DeathDayProps) => { 14 | const deathDay = useDeathDay({ day: props.day }); 15 | 16 | if (!deathDay) { 17 | return null; 18 | } 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | {deathDay} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /ios/CineTasty/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | self.moduleName = @"CineTasty"; 10 | // You can add your custom initial props in the dictionary below. 11 | // They will be passed down to the ViewController used by React Native. 12 | self.initialProps = @{}; 13 | 14 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 15 | } 16 | 17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 18 | { 19 | #if DEBUG 20 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 21 | #else 22 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 23 | #endif 24 | } 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /src/utils/render-svg-icon-conditionally/render-svg-icon-conditionally.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SVGIconProps, SVGIcon } from '@common-components'; 4 | 5 | export type ConditionProps = Omit; 6 | 7 | type RenderSVGIconConditionallyParams = { 8 | ifFalse: ConditionProps; 9 | ifTrue: ConditionProps; 10 | condition: boolean; 11 | }; 12 | 13 | export const renderSVGIconConditionally = ( 14 | props: RenderSVGIconConditionallyParams, 15 | ) => 16 | props.condition ? ( 17 | 23 | ) : ( 24 | 30 | ); 31 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/quiz/Quiz.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { WRAPPER_HEIGHT } from '@/navigation/components/tab-navigator/TabNavigator.styles'; 4 | import { Typography } from '@/components/common'; 5 | 6 | export const Wrapper = styled.View` 7 | width: 100%; 8 | height: 100%; 9 | align-items: center; 10 | justify-content: space-around; 11 | padding-top: ${({ theme }) => theme.metrics.getHeightFromDP('10%')}px; 12 | padding-horizontal: ${({ theme }) => theme.metrics.getWidthFromDP('7%')}px; 13 | padding-bottom: ${WRAPPER_HEIGHT}px; 14 | `; 15 | 16 | export const LargeText = styled(Typography.MediumText).attrs(() => ({ 17 | alignment: 'center', 18 | }))``; 19 | 20 | export const SubText = styled(Typography.SmallText).attrs(({ theme }) => ({ 21 | color: theme.colors.subText, 22 | alignment: 'center', 23 | }))``; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/react-native/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "types": ["react-native", "jest"], 6 | "module": "ES6", 7 | "moduleResolution": "Bundler", 8 | "jsx": "react-native", 9 | "esModuleInterop": true, 10 | "paths": { 11 | "@/*": ["./*"], 12 | "@styles/*": ["./styles/*"], 13 | "@app-types": ["./types/index.ts"], 14 | "@schema-types": ["./types/schema.ts"], 15 | "@hooks": ["./hooks/index.ts"], 16 | "@providers": ["./providers/index.ts"], 17 | "@common-components": ["./components/common/index.ts"], 18 | "@utils": ["./utils/index.ts"], 19 | "@navigation": ["./navigation/index.ts"], 20 | "@stacks": ["./components/stacks/index.ts"], 21 | "@i18n/*": ["./i18n/*"], 22 | "@mocks": ["../__mocks__/index.ts"] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/components/choose-option-section/ChooseOptionSection.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { Typography } from '@/components/common'; 4 | import metrics from '@styles/metrics'; 5 | 6 | export const ICON_SIZE = metrics.getWidthFromDP('7'); 7 | 8 | export const InnerContentWrapper = styled.TouchableOpacity` 9 | width: 100%; 10 | flex-direction: row; 11 | justify-content: space-between; 12 | align-items: center; 13 | padding: ${({ theme }) => theme.metrics.md}px; 14 | border-radius: ${({ theme }) => theme.metrics.sm}px; 15 | background-color: ${({ theme }) => theme.colors.inputBackground}; 16 | `; 17 | 18 | export const OptionText = styled(Typography.SmallText)``; 19 | 20 | export const SectionTitle = styled(Typography.SmallText)` 21 | margin-bottom: ${({ theme }) => theme.metrics.md}px; 22 | `; 23 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/header/header-loading-placeholder/HeaderLoadingPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | 4 | import { LoadingPlaceholder } from '@common-components'; 5 | 6 | import * as Styles from './HeaderLoadingPlaceholder.styles'; 7 | 8 | export const HeaderLoadingPlaceholder = () => ( 9 | 10 | 11 | 15 | 19 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/navigation/components/HeaderTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Translations } from '@i18n/tags'; 4 | import { useTranslation } from '@hooks'; 5 | import { Typography } from '@common-components'; 6 | 7 | type HeaderTitleWithText = { 8 | translationTag?: Translations.Tags; 9 | text: string; 10 | }; 11 | 12 | type HeaderTitleWithTranslationTag = { 13 | translationTag: Translations.Tags; 14 | text?: string; 15 | }; 16 | 17 | type HeaderTitleProps = HeaderTitleWithText | HeaderTitleWithTranslationTag; 18 | 19 | export const HeaderTitle = (props: HeaderTitleProps) => { 20 | const translation = useTranslation(); 21 | 22 | return ( 23 | 24 | {props.translationTag 25 | ? translation.translate(props.translationTag) 26 | : props.text} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/background-image/BackgroundImage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Animated from 'react-native-reanimated'; 3 | 4 | import { useBackgroundImage } from './use-background-image'; 5 | import * as Styles from './BackgroundImage.styles'; 6 | 7 | type BackgroundImageProps = { 8 | image: string; 9 | }; 10 | 11 | export const BackgroundImage = (props: BackgroundImageProps) => { 12 | const backgroundImage = useBackgroundImage({ image: props.image }); 13 | 14 | return ( 15 | 18 | 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/background-image/BackgroundImage.styles.ts: -------------------------------------------------------------------------------- 1 | import LinearGradient from 'react-native-linear-gradient'; 2 | import styled from 'styled-components/native'; 3 | 4 | import metrics from '@styles/metrics'; 5 | 6 | export const DEFAULT_HEIGHT = metrics.getHeightFromDP('75'); 7 | export const DEFAULT_WIDTH = metrics.width; 8 | 9 | export const SmokeShadow = styled(LinearGradient).attrs(({ theme }) => ({ 10 | colors: [ 11 | ...Array(5).fill('transparent'), 12 | theme.colors.backgroundAlphax5, 13 | theme.colors.backgroundAlphax4, 14 | theme.colors.backgroundAlphax3, 15 | theme.colors.backgroundAlphax2, 16 | theme.colors.backgroundAlphax1, 17 | ...Array(5).fill(theme.colors.background), 18 | ], 19 | }))` 20 | width: 100%; 21 | height: 70%; 22 | bottom: 0; 23 | position: absolute; 24 | `; 25 | 26 | export const BackgroundImage = styled.Image` 27 | width: 100%; 28 | height: 100%; 29 | `; 30 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/images-gallery/screen/components/thumbs-list/thumbs-list-item/ThumbsListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { TMDBImage } from '@common-components'; 4 | import metrics from '@/styles/metrics'; 5 | 6 | import * as Styles from './ThumbsListItem.styles'; 7 | 8 | type ThumbsListItemProps = { 9 | onPress: () => void; 10 | isSelected: boolean; 11 | image: string; 12 | }; 13 | 14 | export const ThumbsListItem = (props: ThumbsListItemProps) => ( 15 | 19 | 28 | {props.isSelected && } 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/death-day/use-death-day.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | import { ISO6391Language } from '@/types/schema'; 4 | import { useTranslation } from '@hooks'; 5 | 6 | type UseDeathDayParams = { 7 | day: string; 8 | }; 9 | 10 | export const useDeathDay = (params: UseDeathDayParams) => { 11 | const translation = useTranslation(); 12 | 13 | const deathDay = useMemo(() => { 14 | const splittedDate = params.day.split('-'); 15 | if (splittedDate.length !== 3) { 16 | return; 17 | } 18 | const [year, month, day] = splittedDate; 19 | const isCurrentLanguageEnglish = 20 | translation.currentLanguage.toLocaleLowerCase() === 21 | ISO6391Language.en.toLocaleLowerCase(); 22 | if (isCurrentLanguageEnglish) { 23 | return `${year}-${month}-${day}`; 24 | } 25 | return `${day}/${month}/${year}`; 26 | }, [translation.currentLanguage, params.day]); 27 | 28 | return deathDay; 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/quiz/Quiz.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { RoundedButton } from '@common-components'; 4 | 5 | import { QuizProps } from '../../routes/route-params-types'; 6 | import * as Styles from './Quiz.styles'; 7 | import { useQuiz } from './use-quiz'; 8 | 9 | export const Quiz = (props: QuizProps) => { 10 | const quiz = useQuiz({ navigation: props.navigation }); 11 | 12 | return ( 13 | 14 | 15 | {quiz.texts.welcome} 16 | 17 | 18 | {quiz.texts.description} 19 | 20 | 21 | {quiz.texts.challenge} 22 | 23 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/tv-show-season/screen/scroll-with-animated-header-params.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components/native'; 2 | 3 | import * as HeaderStyles from '../../media-details/common/header/Header.styles'; 4 | 5 | export const scrollWithAnimatedHeaderParams = (theme: DefaultTheme) => ({ 6 | backgroudColor: { 7 | input: [ 8 | 0, 9 | HeaderStyles.POSTER_IMAGE_DEFAULT_HEIGHT / 2, 10 | HeaderStyles.POSTER_IMAGE_DEFAULT_HEIGHT, 11 | ], 12 | output: ['transparent', 'transparent', theme.colors.background], 13 | }, 14 | title: { 15 | input: [ 16 | 0, 17 | HeaderStyles.POSTER_IMAGE_DEFAULT_HEIGHT + 18 | HeaderStyles.MEDIA_HEADLINE_MARGIN_TOP + 19 | HeaderStyles.MARGIN_TOP, 20 | HeaderStyles.POSTER_IMAGE_DEFAULT_HEIGHT + 21 | HeaderStyles.MEDIA_HEADLINE_MARGIN_TOP + 22 | HeaderStyles.MARGIN_TOP + 23 | theme.metrics.xl, 24 | ], 25 | output: [0, 0, 1], 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/common/images-list/images-list-item/ImageListItem.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import styled from 'styled-components/native'; 3 | 4 | import { borderRadius } from '@styles/border-radius'; 5 | import metrics from '@/styles/metrics'; 6 | 7 | export const DEFAULT_ICON_SIZE = metrics.xl * 2; 8 | 9 | export type Orientation = 'PORTRAIT' | 'LANDSCAPE'; 10 | 11 | export type ImageOrientation = { 12 | orientation: Orientation; 13 | }; 14 | 15 | export const Wrapper = styled.TouchableOpacity` 16 | margin-right: ${({ theme }) => theme.metrics.md}px; 17 | `; 18 | 19 | export const makeImageStyle = (orientation: Orientation) => { 20 | const width = orientation === 'PORTRAIT' ? '36' : '60'; 21 | const height = orientation === 'PORTRAIT' ? '44' : '36'; 22 | return StyleSheet.create({ 23 | image: { 24 | width: metrics.getWidthFromDP(width), 25 | height: metrics.getWidthFromDP(height), 26 | borderRadius: borderRadius.xs, 27 | }, 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/providers/alert-message/AlertMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Icons } from '@/components/common'; 4 | import STYLES_CONSTANTS from '@styles/constants'; 5 | 6 | import { useAlertMessage } from './use-alert-message'; 7 | import * as Styles from './AlertMessage.styles'; 8 | 9 | type AlertMessageProps = { 10 | onFinishToShow?: () => void; 11 | icon?: Icons; 12 | message: string; 13 | }; 14 | 15 | export const AlertMessage = (props: AlertMessageProps) => { 16 | const alertMessage = useAlertMessage({ 17 | onFinishToShow: props.onFinishToShow, 18 | }); 19 | 20 | return ( 21 | 24 | {props.icon && ( 25 | 26 | )} 27 | 28 | {props.message} 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/common/images-list/images-list-item/ImageListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | 3 | import { TMDBImage } from '@common-components'; 4 | 5 | import * as Styles from './ImageListItem.styles'; 6 | 7 | type ImageListItemProps = Styles.ImageOrientation & { 8 | onPress: () => void; 9 | image: string; 10 | }; 11 | 12 | export const ImageListItem = (props: ImageListItemProps) => { 13 | const imageStyles = useMemo( 14 | () => Styles.makeImageStyle(props.orientation), 15 | [props.orientation], 16 | ); 17 | 18 | return ( 19 | 20 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/common/default-tmdb-list-item/DefaultTMDBListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { Icons, TMDBImage } from '@common-components'; 4 | 5 | import * as Styles from './DefaultTMDBListItem.styles'; 6 | 7 | type DefaultTMDBListItemProps = { 8 | iconImageLoading: Icons; 9 | iconImageError: Icons; 10 | onPress: () => void; 11 | testID: string; 12 | image: string; 13 | title: string; 14 | }; 15 | 16 | export const DefaultTMDBListItem = memo( 17 | (props: DefaultTMDBListItemProps) => ( 18 | 19 | 27 | {props.title} 28 | 29 | ), 30 | () => true, 31 | ); 32 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export { BackgroundImage } from './background-image/BackgroundImage'; 4 | export { MediaDetailsLoading } from './loading/MediaDetailsLoading'; 5 | export { Header } from './header/Header'; 6 | export { MediaInfo } from './media-info/MediaInfo'; 7 | export { Videos } from './videos/Videos'; 8 | export { ParticipantsList } from './participants-list/ParticipantsList'; 9 | 10 | export const TextContentWrapper = styled.View` 11 | padding-top: ${({ theme }) => theme.metrics.lg}px; 12 | background-color: ${({ theme }) => theme.colors.background}; 13 | `; 14 | 15 | export const SectionContentWrapper = styled.View` 16 | margin-top: ${({ theme }) => theme.metrics.sm}px; 17 | `; 18 | 19 | export const SectionWrapper = styled.View` 20 | margin-top: ${({ theme }) => theme.metrics.lg * 2}px; 21 | `; 22 | 23 | export const OverviewWrapper = styled.View` 24 | padding-horizontal: ${({ theme }) => theme.metrics.md}px; 25 | `; 26 | -------------------------------------------------------------------------------- /src/navigation/components/tab-navigator/TabNavigator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BottomTabBarProps } from '@react-navigation/bottom-tabs'; 3 | 4 | import { TabNavigatorItem } from './tab-navigator-item/TabNavigatorItem'; 5 | import * as Styles from './TabNavigator.styles'; 6 | import { useTabNavigator } from './use-tab-navigator'; 7 | 8 | export const TabNavigator = (props: BottomTabBarProps) => { 9 | const tabNavigator = useTabNavigator(props); 10 | 11 | return ( 12 | 13 | {tabNavigator.tabs.map((item, index) => ( 14 | 16 | props.navigation.navigate(props.state.routeNames[index]) 17 | } 18 | title={item.title} 19 | isSelected={index === props.state.index} 20 | inactiveIcon={item.inactiveIcon} 21 | activeIcon={item.activeIcon} 22 | key={item.id} 23 | /> 24 | ))} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/navigation/components/tab-navigator/tabs.ts: -------------------------------------------------------------------------------- 1 | import { Icons } from '@common-components'; 2 | 3 | import { Routes } from '../../routes'; 4 | 5 | type TabRoutes = 6 | | Routes.Tabs.HOME 7 | | Routes.Tabs.FAMOUS 8 | | Routes.Tabs.QUIZ 9 | | Routes.Tabs.NEWS; 10 | 11 | export type TabRouteIds = 'home' | 'famous' | 'quiz' | 'news'; 12 | 13 | export type TabNavigatorItem = { 14 | id: TabRoutes; 15 | inactiveIcon: Icons; 16 | activeIcon: Icons; 17 | }; 18 | 19 | const tabs: TabNavigatorItem[] = [ 20 | { 21 | id: Routes.Tabs.HOME, 22 | activeIcon: 'home-active', 23 | inactiveIcon: 'home-inactive', 24 | }, 25 | { 26 | id: Routes.Tabs.FAMOUS, 27 | activeIcon: 'famous-active', 28 | inactiveIcon: 'famous-inactive', 29 | }, 30 | { 31 | id: Routes.Tabs.QUIZ, 32 | activeIcon: 'quiz-active', 33 | inactiveIcon: 'quiz-inactive', 34 | }, 35 | { 36 | id: Routes.Tabs.NEWS, 37 | activeIcon: 'news-active', 38 | inactiveIcon: 'news-inactive', 39 | }, 40 | ]; 41 | 42 | export default tabs; 43 | -------------------------------------------------------------------------------- /src/components/common/default-tmdb-list-loading/DefaultTMDBListLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | 4 | import * as DefaultTMDBListItemStyles from '../default-tmdb-list-item/DefaultTMDBListItem.styles'; 5 | import * as Styles from './DefaultTMDBListLoading.styles'; 6 | import { LoadingPlaceholder } from '..'; 7 | 8 | export const DefaultTMDBListLoading = () => ( 9 | 10 | {Array(Styles.NUMBER_OF_LOADING_ITEMS) 11 | .fill({}) 12 | .map((_, index) => ( 13 | 14 | 18 | 22 | 23 | ))} 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/news-list-item/news-image/NewsImage.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import styled from 'styled-components/native'; 3 | import FastImage from 'react-native-fast-image'; 4 | 5 | import metrics from '@styles/metrics'; 6 | import { dark } from '@styles/themes'; 7 | 8 | import { imageWrapper } from '../NewsListItem.styles'; 9 | 10 | export const DEFAULT_ICON_SIZE = metrics.getWidthFromDP('12'); 11 | 12 | export const ImageContent = styled(FastImage)` 13 | width: ${imageWrapper.width}px; 14 | height: ${imageWrapper.height}px; 15 | border-radius: ${imageWrapper.borderRadius}px; 16 | `; 17 | 18 | export const AnimatedViewStlyes = StyleSheet.create({ 19 | fallbackImageWrapper: { 20 | width: imageWrapper.width, 21 | height: imageWrapper.height, 22 | justifyContent: 'center', 23 | alignItems: 'center', 24 | position: 'absolute', 25 | borderRadius: imageWrapper.borderRadius, 26 | backgroundColor: dark.colors.fallbackImageBackground, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/common/header-icon-button/HeaderIconButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Colors } from 'styled-components/native'; 3 | 4 | import { Icons } from '@common-components'; 5 | import metrics from '@/styles/metrics'; 6 | 7 | import * as Styles from './HeaderIconButton.styles'; 8 | import { SVGIcon } from '../svg-icon'; 9 | 10 | export type HeaderIconButtonProps = { 11 | withMarginRight?: boolean; 12 | withMarginLeft?: boolean; 13 | color?: keyof Colors; 14 | onPress: () => void; 15 | disabled?: boolean; 16 | iconName: Icons; 17 | }; 18 | 19 | const iconSize = metrics.getWidthFromDP('6'); 20 | 21 | export const HeaderIconButton = (props: HeaderIconButtonProps) => ( 22 | 28 | 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /src/hooks/use-tmdb-image-uri/use-tmdb-image-uri.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { ImageType } from '@/providers/tmdb-image-qualities/types'; 4 | import { useTMDBImageQualities } from '@providers'; 5 | 6 | export const BASE_URL = 'https://image.tmdb.org/t/p'; 7 | export const THUMBNAIL_URL = `${BASE_URL}/w154`; 8 | 9 | type GetURIParams = { 10 | isThumbnail?: boolean; 11 | imageType: ImageType; 12 | image: string; 13 | }; 14 | 15 | export const useTMDBImageURI = () => { 16 | const tmdbImagesQualities = useTMDBImageQualities(); 17 | 18 | const uri = useCallback( 19 | (params: GetURIParams) => { 20 | if (params.isThumbnail) { 21 | return `${THUMBNAIL_URL}${params.image}`; 22 | } 23 | if (tmdbImagesQualities.mappingImageTypeToImageSize) { 24 | return `${BASE_URL}/${ 25 | tmdbImagesQualities.mappingImageTypeToImageSize[params.imageType] 26 | }${params.image}`; 27 | } 28 | }, 29 | [tmdbImagesQualities.mappingImageTypeToImageSize], 30 | ); 31 | 32 | return { uri }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/storage/storage.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | 3 | export const APP_STORAGE_KEY = '@CINE_TASTY'; 4 | 5 | export const storage = { 6 | get: async (key: string) => { 7 | try { 8 | const valueFromStorage = await AsyncStorage.getItem( 9 | `${APP_STORAGE_KEY}:${key}`, 10 | ); 11 | if (!valueFromStorage) { 12 | return; 13 | } 14 | return JSON.parse(valueFromStorage) as T; 15 | } catch (err) { 16 | return undefined; 17 | } 18 | }, 19 | 20 | set: async (key: string, value: unknown) => { 21 | try { 22 | await AsyncStorage.setItem( 23 | `${APP_STORAGE_KEY}:${key}`, 24 | JSON.stringify(value), 25 | ); 26 | return true; 27 | } catch (err) { 28 | return false; 29 | } 30 | }, 31 | 32 | delete: async (key: string) => { 33 | try { 34 | await AsyncStorage.removeItem(`${APP_STORAGE_KEY}:${key}`); 35 | return true; 36 | } catch (err) { 37 | return false; 38 | } 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/questions/components/question-wrapper/QuestionWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | import GLOBAL_STYLES from '@styles/constants'; 4 | 5 | import * as Styles from './QuestionWrapper.styles'; 6 | 7 | type ListItemWrapperProps = { 8 | currentQuestionIndex: number; 9 | numberOfQuestions: number; 10 | children: ReactNode; 11 | question: string; 12 | }; 13 | 14 | export const QuestionWrapper = (props: ListItemWrapperProps) => ( 15 | 16 | 17 | 18 | 19 | {`${props.currentQuestionIndex}/${props.numberOfQuestions}`} 20 | 21 | 22 | {props.question} 23 | 24 | 25 | {props.children} 26 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/types.ts: -------------------------------------------------------------------------------- 1 | import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack'; 2 | 3 | import { Routes } from '@navigation'; 4 | 5 | import { FamousStackRoutes } from '../../famous/routes/route-params-types'; 6 | import { HomeStackRoutes } from '../../home/routes/route-params-types'; 7 | 8 | export enum SearchType { 9 | MOVIE = 'MOVIE', 10 | FAMOUS = 'FAMOUS', 11 | TV = 'TV', 12 | } 13 | 14 | export type SearchItem = { 15 | title?: string | null; 16 | image?: string | null; 17 | id?: number | null; 18 | }; 19 | 20 | type SearchStack = FamousStackRoutes & HomeStackRoutes; 21 | 22 | export type SearchEntryRoutes = 23 | | Routes.Famous.SEARCH_FAMOUS 24 | | Routes.Home.SEARCH_MOVIE 25 | | Routes.Home.SEARCH_TV_SHOW; 26 | 27 | export type SearchNavigationProps = StackScreenProps< 28 | SearchStack, 29 | SearchEntryRoutes 30 | >; 31 | 32 | export type SearchNavigationProp = StackNavigationProp< 33 | SearchStack, 34 | SearchEntryRoutes 35 | >; 36 | 37 | export type SearchProps = { 38 | type: SearchType; 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/participants-list/ParticipantsList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ParticipantListItem } from './participants-list-item/ParticipantsListItem'; 4 | import * as Styles from './ParticipantsList.styles'; 5 | 6 | type Participant = { 7 | subText?: string | null; 8 | image?: string | null; 9 | name?: string | null; 10 | id: number; 11 | }; 12 | 13 | type ParticipantsListProps = { 14 | onPress: (participant: Participant) => void; 15 | participants: Participant[]; 16 | }; 17 | 18 | export const ParticipantsList = (props: ParticipantsListProps) => ( 19 | `${item.id}`} 22 | testID="participants-list" 23 | horizontal 24 | showsHorizontalScrollIndicator={false} 25 | renderItem={({ item }) => ( 26 | props.onPress(item)} 28 | subText={item.subText || ''} 29 | image={item.image || ''} 30 | name={item.name || '-'} 31 | /> 32 | )} 33 | /> 34 | ); 35 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18next, { LanguageDetectorAsyncModule } from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | 4 | import { supportedLanguages } from './supported-languages'; 5 | import { languageDetection } from './language-detection'; 6 | import { pt } from './locale/pt'; 7 | import { es } from './locale/es'; 8 | import { en } from './locale/en'; 9 | 10 | const FALLBACK_LANGUAGE = 'en'; 11 | 12 | const languageDetector: LanguageDetectorAsyncModule = { 13 | type: 'languageDetector', 14 | async: true, 15 | detect: async () => languageDetection(FALLBACK_LANGUAGE, supportedLanguages), 16 | cacheUserLanguage: () => {}, 17 | init: () => {}, 18 | }; 19 | 20 | i18next 21 | .use(languageDetector) 22 | .use(initReactI18next) 23 | .init({ 24 | compatibilityJSON: 'v3', 25 | fallbackLng: FALLBACK_LANGUAGE, 26 | debug: __DEV__, 27 | resources: { 28 | en: { 29 | translations: en, 30 | }, 31 | pt: { 32 | translations: pt, 33 | }, 34 | es: { 35 | translations: es, 36 | }, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/utils/format-date/format-date.test.ts: -------------------------------------------------------------------------------- 1 | import { formatDate } from './format-date'; 2 | 3 | describe('Utils/format-date', () => { 4 | describe('When formating successfully', () => { 5 | it('should return correctly when the language selected is "en"', () => { 6 | const formattedDate = formatDate('en', '1994-02-21'); 7 | expect(formattedDate).toEqual('1994-02-21'); 8 | }); 9 | 10 | it('should return correctly when the language selected is other than "en"', () => { 11 | const formattedDate = formatDate('pt', '1994-02-21'); 12 | expect(formattedDate).toEqual('21/02/1994'); 13 | }); 14 | }); 15 | 16 | describe('When formating with error', () => { 17 | it('should return correctly when "date" is not defined', () => { 18 | const formattedDate = formatDate('en'); 19 | expect(formattedDate).toEqual('-'); 20 | }); 21 | 22 | it('should return correctly when "date" has an unexpected format', () => { 23 | const formattedDate = formatDate('en', '21/02-1994'); 24 | expect(formattedDate).toEqual('-'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/common/loading-placeholder/LoadingPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ViewStyle } from 'react-native'; 3 | import { useTheme } from 'styled-components/native'; 4 | import Animated from 'react-native-reanimated'; 5 | 6 | import { useLoadingPlaceholder } from './use-loading-placeholder'; 7 | 8 | export type LoadingPlaceholderProps = { 9 | style?: ViewStyle; 10 | indexToDelayAnimation?: number; 11 | testID?: string; 12 | }; 13 | 14 | export const LoadingPlaceholder = ({ 15 | testID = 'loading-placeholder', 16 | indexToDelayAnimation = 0, 17 | style = {}, 18 | }: LoadingPlaceholderProps) => { 19 | const loadingPlaceholder = useLoadingPlaceholder({ 20 | indexToDelayAnimation: indexToDelayAnimation, 21 | }); 22 | 23 | const theme = useTheme(); 24 | 25 | return ( 26 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/common/images-list/ImagesList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ImageOrientation } from './images-list-item/ImageListItem.styles'; 4 | import { ImageListItem } from './images-list-item/ImageListItem'; 5 | import * as Styles from './ImagesList.styles'; 6 | import { useImagesList } from './use-images-list'; 7 | 8 | type ImagesListProps = ImageOrientation & { 9 | images: string[]; 10 | }; 11 | 12 | export const ImagesList = (props: ImagesListProps) => { 13 | const imagesList = useImagesList({ images: props.images }); 14 | 15 | if (!props.images.length) { 16 | return null; 17 | } 18 | 19 | return ( 20 | ( 22 | imagesList.onPressImage(index)} 24 | orientation={props.orientation} 25 | image={item} 26 | key={item} 27 | /> 28 | )} 29 | showsHorizontalScrollIndicator={false} 30 | keyExtractor={item => item} 31 | horizontal 32 | data={props.images} 33 | testID="images-list" 34 | /> 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/hooks/use-translation/use-translation.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react'; 2 | import { useTranslation as i18nUseTranslation } from 'react-i18next'; 3 | 4 | import { supportedLanguages } from '@i18n/supported-languages'; 5 | import { Translations } from '@i18n/tags'; 6 | 7 | type Options = { 8 | value: number; 9 | }; 10 | 11 | export const useTranslation = () => { 12 | const i18next = i18nUseTranslation(); 13 | 14 | const translate = useCallback( 15 | (key: Translations.Tags, options?: Options) => i18next.t(key, options), 16 | [i18next.t], 17 | ); 18 | 19 | const currentLanguage = useMemo(() => { 20 | switch (i18next.i18n.language) { 21 | case supportedLanguages[0]: 22 | return supportedLanguages[0]; 23 | case supportedLanguages[1]: 24 | return supportedLanguages[1]; 25 | case supportedLanguages[2]: 26 | return supportedLanguages[2]; 27 | default: 28 | return supportedLanguages[0]; 29 | } 30 | }, [i18next.i18n.language]); 31 | 32 | return { 33 | supportedLanguages, 34 | currentLanguage, 35 | translate, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /ios/CineTasty/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/navigation/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTheme } from 'styled-components/native'; 3 | import { NavigationContainer } from '@react-navigation/native'; 4 | import { createStackNavigator } from '@react-navigation/stack'; 5 | 6 | import { Tabs } from './components/Tabs'; 7 | import { Routes } from './routes'; 8 | 9 | const RootStack = createStackNavigator(); 10 | 11 | export const Navigation = () => { 12 | const theme = useTheme(); 13 | 14 | return ( 15 | 27 | 28 | 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/search-config/search-movies-config.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | import { Icons } from '@/components/common'; 4 | import { Translations } from '@/i18n/tags'; 5 | import { Routes } from '@/navigation'; 6 | 7 | import { SearchItem, SearchNavigationProp } from '../types'; 8 | 9 | export const SEARCH_MOVIES_QUERY = gql` 10 | query SearchMovies($input: SearchInput!) { 11 | search: searchMovies(input: $input) { 12 | items { 13 | image: posterPath 14 | title 15 | id 16 | } 17 | hasMore 18 | } 19 | } 20 | `; 21 | 22 | export const searchMoviesConfig = () => ({ 23 | navigateToDetails: ( 24 | searchItem: SearchItem, 25 | navigation: SearchNavigationProp, 26 | ) => { 27 | navigation.navigate(Routes.Home.MOVIE_DETAILS, undefined); 28 | }, 29 | searchPlaceholder: Translations.TrendingFamous.ENTRY_ERROR, 30 | searchByTextError: Translations.TrendingFamous.ENTRY_ERROR, 31 | paginationError: Translations.TrendingFamous.PAGINATION_ERROR, 32 | query: SEARCH_MOVIES_QUERY, 33 | iconImageLoading: 'video-vintage' as Icons, 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/components/choose-option-section/ChooseOptionSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SVGIcon } from '@/components/common'; 4 | 5 | import { SetupQuestionsOptions } from '../../use-setup-questions/use-setup-questions'; 6 | import * as Styles from './ChooseOptionSection.styles'; 7 | 8 | type ChooseOptionSectionProps = { 9 | onPressOption: (section: SetupQuestionsOptions) => void; 10 | section: SetupQuestionsOptions; 11 | selectedOption: string; 12 | title: string; 13 | }; 14 | 15 | export const ChooseOptionSection = (props: ChooseOptionSectionProps) => ( 16 | <> 17 | 18 | {props.title} 19 | 20 | props.onPressOption(props.section)} 22 | testID={`dropdown-button-${props.section}`}> 23 | 24 | {props.selectedOption} 25 | 26 | 27 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /src/components/common/paginated-list-footer/PaginatedListFooter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform } from 'react-native'; 3 | 4 | import metrics from '@styles/metrics'; 5 | 6 | import * as Styles from './PaginatedListFooter.styles'; 7 | import { SVGIcon } from '../svg-icon/SVGIcon'; 8 | 9 | type PaginatedListFooterProps = { 10 | onPressReloadButton: () => void; 11 | isPaginating: boolean; 12 | hasError: boolean; 13 | }; 14 | 15 | export const PaginatedListFooter = (props: PaginatedListFooterProps) => ( 16 | 17 | {props.isPaginating && ( 18 | 25 | )} 26 | {props.hasError && ( 27 | 30 | 31 | 32 | )} 33 | 34 | ); 35 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/search-config/search-config.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from '@apollo/client'; 2 | 3 | import { Icons } from '@/components/common'; 4 | import { Translations } from '@/i18n/tags'; 5 | 6 | import { SearchItem, SearchNavigationProp, SearchType } from '../types'; 7 | import { searchFamousConfig } from './search-famous-config'; 8 | import { searchMoviesConfig } from './search-movies-config'; 9 | import { searchTVShowsConfig } from './search-tv-shows-config'; 10 | 11 | type SearchConfig = { 12 | navigateToDetails: ( 13 | searchItem: SearchItem, 14 | navigation: SearchNavigationProp, 15 | ) => void; 16 | searchPlaceholder: Translations.Tags; 17 | searchByTextError: Translations.Tags; 18 | paginationError: Translations.Tags; 19 | query: DocumentNode; 20 | iconImageLoading: Icons; 21 | }; 22 | 23 | export const getSearchConfig = (searchType: SearchType) => { 24 | const searchTypeConfigMapping: Record = { 25 | MOVIE: searchMoviesConfig(), 26 | TV: searchTVShowsConfig(), 27 | FAMOUS: searchFamousConfig(), 28 | }; 29 | return searchTypeConfigMapping[searchType]; 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/components/number-of-questions/use-number-of-questions.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | import Slider from '@react-native-community/slider'; 3 | 4 | export const SLIDER_MAX_VALUE = 50; 5 | export const SLIDER_MIN_VALUE = 1; 6 | 7 | type UseNumberOfQuestionsProps = { 8 | onSetNumberQuestions: (value: number) => void; 9 | numberOfQuestions: number; 10 | }; 11 | 12 | export const useNumberOfQuestions = (props: UseNumberOfQuestionsProps) => { 13 | const sliderRef = useRef(null); 14 | 15 | const handleChangeValue = useCallback( 16 | (distance: number) => { 17 | props.onSetNumberQuestions(distance); 18 | }, 19 | [props.onSetNumberQuestions], 20 | ); 21 | 22 | const handleOnLayout = useCallback(() => { 23 | sliderRef.current?.setNativeProps({ 24 | value: props.numberOfQuestions, 25 | }); 26 | }, [props.numberOfQuestions]); 27 | 28 | return { 29 | onValueChange: handleChangeValue, 30 | onLayout: handleOnLayout, 31 | sliderMaxValue: SLIDER_MAX_VALUE, 32 | sliderMinValue: SLIDER_MIN_VALUE, 33 | sliderRef, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/search-config/search-tv-shows-config.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | import { Icons } from '@/components/common'; 4 | import { Translations } from '@/i18n/tags'; 5 | import { Routes } from '@/navigation'; 6 | 7 | import { SearchItem, SearchNavigationProp } from '../types'; 8 | 9 | export const SEARCH_TV_SHOWS_QUERY = gql` 10 | query SearchTVShows($input: SearchInput!) { 11 | search: searchTVShows(input: $input) { 12 | hasMore 13 | items { 14 | image: posterPath 15 | title: name 16 | id 17 | } 18 | } 19 | } 20 | `; 21 | 22 | export const searchTVShowsConfig = () => ({ 23 | navigateToDetails: ( 24 | searchItem: SearchItem, 25 | navigation: SearchNavigationProp, 26 | ) => { 27 | navigation.navigate(Routes.Home.TV_SHOW_DETAILS, undefined); 28 | }, 29 | searchPlaceholder: Translations.TrendingFamous.ENTRY_ERROR, 30 | searchByTextError: Translations.TrendingFamous.ENTRY_ERROR, 31 | paginationError: Translations.TrendingFamous.PAGINATION_ERROR, 32 | query: SEARCH_TV_SHOWS_QUERY, 33 | iconImageLoading: 'video-vintage' as Icons, 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/common/loading-placeholder/use-loading-placeholder.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | import { 3 | useAnimatedStyle, 4 | withTiming, 5 | withRepeat, 6 | useSharedValue, 7 | } from 'react-native-reanimated'; 8 | 9 | type UseLoadingPlaceholderProps = { 10 | indexToDelayAnimation: number; 11 | }; 12 | 13 | export const ANIMATION_DURATION = 800; 14 | const EXTRA_ANIMATION_DURATION = 50; 15 | 16 | export const useLoadingPlaceholder = (props: UseLoadingPlaceholderProps) => { 17 | const animatedOpacity = useSharedValue(1); 18 | 19 | const style = useAnimatedStyle(() => { 20 | return { 21 | opacity: animatedOpacity.value, 22 | }; 23 | }); 24 | 25 | const animateOpacity = useCallback(() => { 26 | animatedOpacity.value = withRepeat( 27 | withTiming(0.1, { 28 | duration: 29 | ANIMATION_DURATION + 30 | props.indexToDelayAnimation * EXTRA_ANIMATION_DURATION, 31 | }), 32 | -1, 33 | true, 34 | ); 35 | }, [props.indexToDelayAnimation]); 36 | 37 | useEffect(() => { 38 | animateOpacity(); 39 | }, []); 40 | 41 | return { 42 | style, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/videos/Videos.styles.ts: -------------------------------------------------------------------------------- 1 | import FastImage from 'react-native-fast-image'; 2 | import styled from 'styled-components/native'; 3 | 4 | export const VideoListItemWrapper = styled.TouchableOpacity` 5 | width: ${({ theme }) => theme.metrics.getWidthFromDP('50')}px; 6 | height: ${({ theme }) => theme.metrics.getWidthFromDP('36')}px; 7 | margin-right: ${({ theme }) => theme.metrics.md}px; 8 | border-radius: ${({ theme }) => theme.metrics.sm}px; 9 | `; 10 | 11 | export const IconWrapper = styled.View` 12 | width: 100%; 13 | height: 100%; 14 | justify-content: center; 15 | align-items: center; 16 | position: absolute; 17 | background-color: rgba(0, 0, 0, 0.2); 18 | border-radius: ${({ theme }) => theme.metrics.sm}px; 19 | `; 20 | 21 | export const Image = styled(FastImage)` 22 | width: 100%; 23 | height: 100%; 24 | border-radius: ${({ theme }) => theme.metrics.sm}px; 25 | `; 26 | 27 | export const List = styled.ScrollView.attrs(({ theme }) => ({ 28 | contentContainerStyle: { 29 | paddingLeft: theme.metrics.md, 30 | }, 31 | }))` 32 | margin-top: ${({ theme }) => theme.metrics.md}px; 33 | `; 34 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/tv-show-season/screen/components/tv-show-season-loading/TVShowSeasonLoading.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import metrics from '@/styles/metrics'; 5 | 6 | export const LoadingEpisodeWrapper = styled.View` 7 | padding-horizontal: ${({ theme }) => theme.metrics.md}px; 8 | `; 9 | 10 | export const sheet = StyleSheet.create({ 11 | title: { 12 | width: '85%', 13 | height: metrics.xl * 1.8, 14 | borderRadius: metrics.sm, 15 | }, 16 | season: { 17 | width: '50%', 18 | height: metrics.xl * 1.8, 19 | borderRadius: metrics.sm, 20 | marginTop: metrics.sm, 21 | }, 22 | votes: { 23 | width: '25%', 24 | height: metrics.xl * 1.8, 25 | borderRadius: metrics.sm, 26 | marginTop: metrics.sm, 27 | }, 28 | episodeHalfLength: { 29 | width: '75%', 30 | height: metrics.xl * 1.8, 31 | borderRadius: metrics.sm, 32 | marginTop: metrics.sm, 33 | }, 34 | episodeFullLength: { 35 | width: '100%', 36 | height: metrics.xl * 1.8, 37 | borderRadius: metrics.sm, 38 | marginTop: metrics.sm, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/components/search-bar/SearchBar.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { getStatusBarHeight } from '@utils'; 4 | 5 | import { dark } from '@styles/themes'; 6 | 7 | const HEADER_HEIGHT = 44; 8 | 9 | export const Wrapper = styled.View` 10 | width: 100%; 11 | height: ${() => getStatusBarHeight() + HEADER_HEIGHT}px; 12 | justify-content: flex-end; 13 | padding-bottom: ${({ theme }) => theme.metrics.md}px; 14 | background-color: ${({ theme }) => theme.colors.contrast}; 15 | `; 16 | 17 | export const ContentWrapper = styled.View` 18 | flex-direction: row; 19 | `; 20 | 21 | export const Input = styled.TextInput.attrs(({ placeholder, theme }) => ({ 22 | placeholderTextColor: dark.colors.subText, 23 | selectionColor: theme.colors.primary, 24 | underlineColorAndroid: 'transparent', 25 | returnKeyLabel: 'search', 26 | returnKeyType: 'search', 27 | numberOfLines: 1, 28 | autoFocus: true, 29 | placeholder, 30 | }))` 31 | width: 85%; 32 | margin-left: ${({ theme }) => theme.metrics.sm}px; 33 | font-family: CircularStd-Book; 34 | font-size: ${({ theme }) => theme.metrics.xl}px; 35 | color: white; 36 | `; 37 | -------------------------------------------------------------------------------- /src/components/stacks/home/routes/route-params-types.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@/navigation'; 2 | 3 | import { FamousDetailsProps } from '../../common-screens/famous-details/routes/route-params-types'; 4 | import { SearchProps } from '../../common-screens/search/types'; 5 | import { ImagesGalleryNavigationProps } from '../../common-screens/images-gallery/routes/route-params-types'; 6 | import { TVShowDetailsNavigationProps } from '../../common-screens/media-details/tv-show-details/routes/route-params-types'; 7 | import { TVShowSeasonNavigationProps } from '../../common-screens/tv-show-season/routes/route-params-types'; 8 | import { MovieDetailsNavigationProps } from '../../common-screens/media-details/movie-details/routes/route-params-types'; 9 | 10 | export type HomeStackRoutes = { 11 | [Routes.Home.SEARCH_MOVIE]: SearchProps; 12 | [Routes.Home.SEARCH_TV_SHOW]: SearchProps; 13 | [Routes.Home.TV_SHOW_DETAILS]: TVShowDetailsNavigationProps; 14 | [Routes.Home.FAMOUS_DETAILS]: FamousDetailsProps; 15 | [Routes.Home.MOVIE_DETAILS]: MovieDetailsNavigationProps; 16 | [Routes.Home.IMAGES_GALLERY]: ImagesGalleryNavigationProps; 17 | [Routes.Home.TV_SHOW_SEASON]: TVShowSeasonNavigationProps; 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/media-info/MediaInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTheme } from 'styled-components/native'; 3 | 4 | import { Typography } from '@/components/common'; 5 | 6 | import * as Styles from './MediaInfo.styles'; 7 | 8 | type Info = { 9 | title: string; 10 | value: string; 11 | }; 12 | 13 | type MediaInfoProps = { 14 | infos: Info[]; 15 | }; 16 | 17 | export const MediaInfo = (props: MediaInfoProps) => { 18 | const theme = useTheme(); 19 | 20 | if (!props.infos.length) { 21 | return null; 22 | } 23 | 24 | return ( 25 | 26 | {props.infos.map((info, index) => ( 27 | 28 | 29 | {info.title} 30 | 31 | 34 | {info.value} 35 | 36 | 37 | ))} 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/questions/components/multi-choice-question/use-multi-choice-question.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState, useMemo } from 'react'; 2 | 3 | import { useTranslation } from '@hooks'; 4 | import { Translations } from '@i18n/tags'; 5 | 6 | type UseMultiChoiceQuestionProps = { 7 | onPressNext: (answerSelected: string) => void; 8 | }; 9 | 10 | export const useMultiChoiceQuestion = (props: UseMultiChoiceQuestionProps) => { 11 | const [selectedOption, setSelectedOption] = useState(''); 12 | 13 | const translations = useTranslation(); 14 | 15 | const handlePressNext = useCallback(() => { 16 | props.onPressNext(selectedOption); 17 | }, [props.onPressNext, selectedOption]); 18 | 19 | const handleSelectOption = useCallback((option: string) => { 20 | setSelectedOption(option); 21 | }, []); 22 | 23 | const texts = useMemo( 24 | () => ({ 25 | next: translations.translate(Translations.Quiz.QUIZ_NEXT), 26 | }), 27 | [translations.translate], 28 | ); 29 | 30 | return { 31 | isNextButtonDisabled: !selectedOption, 32 | onSelectOption: handleSelectOption, 33 | onPressNext: handlePressNext, 34 | selectedOption, 35 | texts, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/common/advice/Advice.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SVGIcon, Icons } from '@common-components'; 4 | import metrics from '@styles/metrics'; 5 | 6 | import * as Styles from './Advice.styles'; 7 | 8 | type AdviceProps = { 9 | icon: Icons; 10 | withMarginTop?: boolean; 11 | description: string; 12 | suggestion: string; 13 | title: string; 14 | }; 15 | 16 | export const Advice = (props: AdviceProps) => ( 17 | 18 | 19 | 24 | 25 | {!!props.title && ( 26 | {props.title} 27 | )} 28 | {!!props.description && ( 29 | 30 | {props.description} 31 | 32 | )} 33 | {!!props.suggestion && ( 34 | 35 | {props.suggestion} 36 | 37 | )} 38 | 39 | ); 40 | -------------------------------------------------------------------------------- /src/navigation/use-default-header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from 'react'; 2 | import { useNavigation } from '@react-navigation/native'; 3 | import { useTheme } from 'styled-components/native'; 4 | 5 | import { HeaderTitle, getTransparentHeaderOptions } from '@navigation'; 6 | import { HeaderIconButton } from '@common-components'; 7 | 8 | type UseDefaultHeaderParams = { 9 | title: string; 10 | }; 11 | 12 | export const useDefaultHeader = (params: UseDefaultHeaderParams) => { 13 | const navigation = useNavigation(); 14 | const theme = useTheme(); 15 | 16 | const HeaderLeft = useCallback( 17 | () => ( 18 | 24 | ), 25 | [], 26 | ); 27 | 28 | const Title = useCallback( 29 | () => , 30 | [params.title], 31 | ); 32 | 33 | useEffect(() => { 34 | navigation.setOptions({ 35 | ...getTransparentHeaderOptions(theme), 36 | headerTitle: Title, 37 | headerTitleAlign: 'center', 38 | headerLeft: HeaderLeft, 39 | }); 40 | }, [HeaderLeft, Title, theme]); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/stacks/news/routes/stack-routes.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { createStackNavigator } from '@react-navigation/stack'; 3 | 4 | import { 5 | defaultHeaderStyle, 6 | Routes, 7 | HeaderTitle as CustomHeaderTitle, 8 | } from '@navigation'; 9 | import { Translations } from '@i18n/tags'; 10 | 11 | import { Container } from '../screens/news/News.styles'; 12 | import { News } from '../screens/news/News'; 13 | import { NewsStackProps } from './route-params-types'; 14 | 15 | const Stack = createStackNavigator(); 16 | 17 | const NewsComponent = (props: NewsStackProps) => ( 18 | 19 | 20 | 21 | ); 22 | 23 | export const NewsStack = () => { 24 | const HeaderTitle = useCallback( 25 | () => , 26 | [], 27 | ); 28 | 29 | return ( 30 | 31 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/quiz/use-quiz.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react'; 2 | 3 | import { Translations } from '@i18n/tags'; 4 | import { useTranslation } from '@hooks'; 5 | import { Routes } from '@/navigation'; 6 | 7 | import { QuizPropsNavigationProp } from '../../routes/route-params-types'; 8 | 9 | type UseQuizProps = { 10 | navigation: QuizPropsNavigationProp; 11 | }; 12 | 13 | export const useQuiz = (props: UseQuizProps) => { 14 | const translation = useTranslation(); 15 | 16 | const texts = useMemo( 17 | () => ({ 18 | welcome: translation.translate(Translations.Quiz.QUIZ_WELCOME), 19 | description: translation.translate(Translations.Quiz.QUIZ_DESCRIPTION), 20 | challenge: translation.translate(Translations.Quiz.QUIZ_CHALLENGE), 21 | chooseQuestions: translation.translate( 22 | Translations.Quiz.QUIZ_CHOOSE_QUESTIONS, 23 | ), 24 | }), 25 | [translation.translate], 26 | ); 27 | 28 | const handleNavigateSetupQuestions = useCallback(() => { 29 | props.navigation.navigate(Routes.Quiz.SETUP_QUESTIONS); 30 | }, [props.navigation]); 31 | 32 | return { 33 | onPressChooseQuestions: handleNavigateSetupQuestions, 34 | texts, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/components/setup-questions-modal/option-list-item/OptionListItem.styles.ts: -------------------------------------------------------------------------------- 1 | import { TouchableOpacityProps } from 'react-native'; 2 | import styled, { IStyledComponent } from 'styled-components/native'; 3 | import { Substitute } from 'styled-components/native/dist/types'; 4 | 5 | import { Typography } from '@common-components'; 6 | import { dark } from '@styles/themes'; 7 | 8 | type ListItemStyleProps = { 9 | isSelected: boolean; 10 | }; 11 | 12 | export const ListItemWrapper: IStyledComponent< 13 | 'native', 14 | Substitute 15 | > = styled.TouchableOpacity` 16 | width: 100%; 17 | height: ${({ theme }) => theme.metrics.getWidthFromDP('20')}px; 18 | flex-direction: row; 19 | justify-content: space-between; 20 | align-items: center; 21 | padding-horizontal: ${({ theme }) => theme.metrics.lg}px; 22 | background-color: ${({ isSelected }) => 23 | isSelected ? dark.colors.background : dark.colors.text}; 24 | `; 25 | 26 | export const ListItemText = styled( 27 | Typography.SmallText, 28 | ).attrs(({ isSelected }) => ({ 29 | color: isSelected ? dark.colors.text : dark.colors.buttonText, 30 | }))``; 31 | -------------------------------------------------------------------------------- /__mocks__/MockedNavigator.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType } from 'react'; 2 | import { View } from 'react-native'; 3 | import { NavigationContainer } from '@react-navigation/native'; 4 | import { createStackNavigator } from '@react-navigation/stack'; 5 | import { ThemeProvider } from 'styled-components/native'; 6 | 7 | import { dark as theme } from '@styles/themes/dark'; 8 | 9 | const Stack = createStackNavigator(); 10 | 11 | type MockedNavigatorProps = { 12 | component: ComponentType; 13 | extraScreens?: string[]; 14 | params?: any; 15 | }; 16 | 17 | export const MockedNavigator = (props: MockedNavigatorProps) => ( 18 | 19 | 20 | 21 | 26 | {props.extraScreens?.map(extraScreen => ( 27 | 33 | ))} 34 | 35 | 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /src/components/common/header-icon-button/HeaderIconButton.styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { IStyledComponent } from 'styled-components/native'; 2 | 3 | import { Substitute } from 'styled-components/native/dist/types'; 4 | import { TouchableOpacity, TouchableOpacityProps } from 'react-native'; 5 | import { RefAttributes } from 'react'; 6 | 7 | type WrapperStyleProps = { 8 | withMarginRight?: boolean; 9 | withMarginLeft?: boolean; 10 | }; 11 | 12 | export const Wrapper: IStyledComponent< 13 | 'native', 14 | Substitute< 15 | Substitute< 16 | TouchableOpacityProps, 17 | TouchableOpacityProps & RefAttributes 18 | >, 19 | WrapperStyleProps 20 | > 21 | > = styled.TouchableOpacity.attrs(({ theme }) => ({ 22 | hitSlop: { 23 | top: theme.metrics.md, 24 | right: theme.metrics.md, 25 | bottom: theme.metrics.md, 26 | left: theme.metrics.md, 27 | }, 28 | }))` 29 | margin-right: ${({ theme, withMarginRight }) => 30 | withMarginRight ? theme.metrics.md : 0}px; 31 | margin-left: ${({ theme, withMarginLeft }) => 32 | withMarginLeft ? theme.metrics.md : 0}px; 33 | opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; 34 | justify-content: center; 35 | align-items: center; 36 | `; 37 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/search-config/search-famous-config.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | import { Translations } from '@/i18n/tags'; 4 | import { Icons } from '@/components/common'; 5 | import { Routes } from '@/navigation'; 6 | 7 | import { SearchItem, SearchNavigationProp } from '../types'; 8 | 9 | export const SEARCH_FAMOUS_QUERY = gql` 10 | query SearchFamous($input: SearchInput!) { 11 | search: searchFamous(input: $input) { 12 | items { 13 | image: profilePath 14 | title: name 15 | id 16 | } 17 | hasMore 18 | } 19 | } 20 | `; 21 | 22 | export const searchFamousConfig = () => ({ 23 | navigateToDetails: ( 24 | searchItem: SearchItem, 25 | navigation: SearchNavigationProp, 26 | ) => { 27 | navigation.navigate(Routes.Famous.DETAILS, { 28 | profilePath: searchItem.image || '', 29 | name: searchItem.title || '-', 30 | id: searchItem.id || -1, 31 | }); 32 | }, 33 | searchPlaceholder: Translations.SearchFamous.SEARCHBAR, 34 | searchByTextError: Translations.SearchFamous.ENTRY_ERROR, 35 | paginationError: Translations.SearchFamous.PAGINATION_ERROR, 36 | query: SEARCH_FAMOUS_QUERY, 37 | iconImageLoading: 'account' as Icons, 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/languages-filter-modal/language-filter-list/filter-languages/use-news-filter-languages.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react'; 2 | 3 | import { useTranslation } from '@hooks'; 4 | import { Translations } from '@i18n/tags'; 5 | 6 | import { languages, Language } from './languages'; 7 | 8 | type TranslatedLanguage = Omit & { name: string }; 9 | 10 | export const useNewsFilterLanguages = () => { 11 | const translation = useTranslation(); 12 | 13 | const sortLanguages = useCallback( 14 | (translatedLanguages: TranslatedLanguage[]) => 15 | translatedLanguages.sort((x, y) => 16 | Intl.Collator().compare(x.name, y.name), 17 | ), 18 | [], 19 | ); 20 | 21 | const translateLanguages = useCallback( 22 | () => 23 | languages.map(language => ({ 24 | ...language, 25 | name: translation.translate( 26 | `${Translations.News.LANGUAGES}:${language.name}` as Translations.Tags, 27 | ), 28 | })), 29 | [translation.translate], 30 | ); 31 | 32 | const filterLanguages = useMemo( 33 | () => sortLanguages(translateLanguages()), 34 | [sortLanguages, translateLanguages], 35 | ); 36 | 37 | return filterLanguages; 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/languages-filter-modal/language-filter-list/LanguageFilterList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollView } from 'react-native-gesture-handler'; 3 | 4 | import { NewsLanguage } from '@schema-types'; 5 | 6 | import LanguageListItem from './list-item/LanguageListItem'; 7 | import { useLanguageFilter } from './use-language-filter'; 8 | 9 | type LanguageFilterListProps = { 10 | onSelectLanguage: (language: NewsLanguage) => void; 11 | languageSelected: NewsLanguage; 12 | }; 13 | 14 | export const LanguageFilterList = (props: LanguageFilterListProps) => { 15 | const languageFilter = useLanguageFilter({ 16 | languageSelected: props.languageSelected, 17 | }); 18 | 19 | return ( 20 | 23 | {languageFilter.filterLanguages.map(filterLanguage => ( 24 | props.onSelectLanguage(filterLanguage.id)} 27 | title={filterLanguage.name} 28 | flag={filterLanguage.flag} 29 | key={filterLanguage.id} 30 | /> 31 | ))} 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | ios/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | *.keystore 35 | !debug.keystore 36 | 37 | # node.js 38 | # 39 | node_modules/ 40 | npm-debug.log 41 | yarn-error.log 42 | 43 | # fastlane 44 | # 45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 46 | # screenshots whenever they are needed. 47 | # For more information about the recommended setup visit: 48 | # https://docs.fastlane.tools/best-practices/source-control/ 49 | 50 | **/fastlane/report.xml 51 | **/fastlane/Preview.html 52 | **/fastlane/screenshots 53 | **/fastlane/test_output 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # Ruby / CocoaPods 59 | /ios/Pods/ 60 | /vendor/bundle/ 61 | 62 | # Temporary files created by Metro to check the health of the file watcher 63 | .metro-health-check* 64 | 65 | # testing 66 | /coverage 67 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/news-list-item/NewsListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { NewsImage } from './news-image/NewsImage'; 4 | import * as Styles from './NewsListItem.styles'; 5 | import { DateDiff } from './date-diff/DateDiff'; 6 | import { useNewsListItem } from './use-news-list-item'; 7 | 8 | type NewsListItemProps = { 9 | source: string; 10 | image: string; 11 | text: string; 12 | date: string; 13 | url?: string; 14 | }; 15 | 16 | export const NewsListItem = memo((props: NewsListItemProps) => { 17 | const newsListItem = useNewsListItem({ url: props.url }); 18 | 19 | return ( 20 | 23 | 24 | 25 | 26 | {props.source} 27 | 28 | 32 | {props.text} 33 | 34 | 35 | 36 | 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /src/utils/status-bar-height/get-statusbar-height-ios.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getStatusBarHeight, 3 | IOS_IPHONE_X_AND_ABOVE, 4 | IOS_BELOW_IPHONE_X, 5 | } from './get-statusbar-height'; 6 | 7 | const mockIsEqualsOrLargerThanIphoneX = jest.fn(); 8 | 9 | jest.mock( 10 | '../is-equals-or-larger-than-iphonex/is-equals-or-larger-than-iphonex', 11 | () => ({ 12 | isEqualsOrLargerThanIphoneX: () => mockIsEqualsOrLargerThanIphoneX(), 13 | }), 14 | ); 15 | 16 | describe('Utils/status-bar-height # iOS', () => { 17 | beforeEach(() => { 18 | jest.resetModules(); 19 | jest.clearAllMocks(); 20 | }); 21 | 22 | it('should return correctly when the "device has iPhoneX dimensions or above"', () => { 23 | mockIsEqualsOrLargerThanIphoneX.mockReturnValue(true); 24 | const statusBarHeight = getStatusBarHeight(); 25 | expect(statusBarHeight).toEqual(IOS_IPHONE_X_AND_ABOVE); 26 | }); 27 | 28 | it('should return correctly hen the "device is below iPhoneX dimensions"', () => { 29 | mockIsEqualsOrLargerThanIphoneX.mockReturnValue(false); 30 | jest.mock('react-native/Libraries/Utilities/Platform', () => ({ 31 | OS: 'ios', 32 | })); 33 | const statusBarHeight = getStatusBarHeight(); 34 | expect(statusBarHeight).toEqual(IOS_BELOW_IPHONE_X); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/styles/themes/light.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components/native'; 2 | 3 | import { ThemeId } from '@app-types'; 4 | 5 | import { borderRadius } from '../border-radius'; 6 | import typography from '../typography'; 7 | import metrics from '../metrics'; 8 | 9 | export const light: DefaultTheme = { 10 | id: ThemeId.LIGHT, 11 | colors: { 12 | primary: '#FFD700', 13 | secondary: '#FFFFFF', 14 | background: '#F0F0F0', 15 | backgroundAlphax1: 'rgba(242,242,242, 0.8)', 16 | backgroundAlphax2: 'rgba(242,242,242, 0.6)', 17 | backgroundAlphax3: 'rgba(242,242,242, 0.4)', 18 | backgroundAlphax4: 'rgba(242,242,242, 0.2)', 19 | backgroundAlphax5: 'rgba(242,242,242, 0.1)', 20 | contrast: '#4d4d4d', 21 | text: '#111111', 22 | subText: '#4d4d4d', 23 | androidToolbar: '#F2F2F2', 24 | inactiveWhite: '#AAAAAA', 25 | loadingColor: '#AAAAAA', 26 | darkLayer: 'rgba(0, 0, 0, 0.4)', 27 | popup: 'rgba(0, 0, 0, 0.9)', 28 | fallbackImageBackground: '#cfcfcf', 29 | fallbackImageIcon: '#4d4d4d', 30 | searchBar: '#4d4d4d', 31 | buttonText: '#262626', 32 | inputBackground: '#CCCCCC', 33 | red: '#D5233B', 34 | green: '#32BE70', 35 | white: '#FFFFFF', 36 | }, 37 | metrics, 38 | borderRadius, 39 | typography, 40 | }; 41 | -------------------------------------------------------------------------------- /src/styles/themes/dark.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components/native'; 2 | 3 | import { ThemeId } from '@app-types'; 4 | 5 | import { borderRadius } from '../border-radius'; 6 | import typography from '../typography'; 7 | import metrics from '../metrics'; 8 | 9 | export const dark: DefaultTheme = { 10 | id: ThemeId.DARK, 11 | colors: { 12 | primary: '#FFD700', 13 | secondary: '#111111', 14 | background: '#222222', 15 | backgroundAlphax1: 'rgba(34, 34, 34, 0.8)', 16 | backgroundAlphax2: 'rgba(34, 34, 34, 0.6)', 17 | backgroundAlphax3: 'rgba(34, 34, 34, 0.4)', 18 | backgroundAlphax4: 'rgba(34, 34, 34, 0.2)', 19 | backgroundAlphax5: 'rgba(34, 34, 34, 0.1)', 20 | contrast: '#4d4d4d', 21 | text: '#FFFFFF', 22 | subText: 'rgba(255, 255, 255, 0.5)', 23 | androidToolbar: '#222', 24 | inactiveWhite: '#AAAAAA', 25 | loadingColor: '#4A4A4A', 26 | darkLayer: 'rgba(0, 0, 0, 0.4)', 27 | popup: 'rgba(0, 0, 0, 0.9)', 28 | fallbackImageBackground: '#cfcfcf', 29 | fallbackImageIcon: '#4d4d4d', 30 | buttonText: '#262626', 31 | inputBackground: '#4d4d4d', 32 | searchBar: '#4d4d4d', 33 | red: '#D5233B', 34 | green: '#32BE70', 35 | white: '#FFFFFF', 36 | }, 37 | metrics, 38 | borderRadius, 39 | typography, 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/tv-show-season/screen/components/episode-details-modal/EpisodeDetailsModal.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import styled from 'styled-components/native'; 3 | 4 | import { borderRadius } from '@/styles/border-radius'; 5 | import { Typography } from '@common-components'; 6 | import metrics from '@/styles/metrics'; 7 | 8 | export const IMAGE_ICON_SIZE = metrics.xl; 9 | 10 | export const Wrapper = styled.View` 11 | padding-top: ${({ theme }) => theme.metrics.md}px; 12 | padding-horizontal: ${({ theme }) => theme.metrics.md}px; 13 | `; 14 | 15 | export const EpisodeTitle = styled(Typography.SmallText).attrs(({ theme }) => ({ 16 | color: theme.colors.buttonText, 17 | bold: true, 18 | }))` 19 | margin-bottom: ${({ theme }) => theme.metrics.xs}px; 20 | `; 21 | 22 | export const Row = styled.View` 23 | flex-direction: row; 24 | margin-bottom: ${({ theme }) => theme.metrics.md}px; 25 | `; 26 | 27 | export const TextWrapper = styled.View` 28 | width: 60%; 29 | justify-content: center; 30 | padding-horizontal: ${({ theme }) => theme.metrics.sm}px; 31 | `; 32 | 33 | export const sheet = StyleSheet.create({ 34 | image: { 35 | width: metrics.xl * 7, 36 | height: metrics.xl * 5, 37 | borderRadius: borderRadius.sm, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/death-day/DeathDay-en.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RenderAPI, render } from '@testing-library/react-native'; 3 | 4 | import { ThemeProvider } from 'styled-components/native'; 5 | import { dark as theme } from '@styles/themes'; 6 | 7 | import { DeathDay } from './DeathDay'; 8 | 9 | jest.mock('@hooks', () => ({ 10 | useTranslation: () => ({ 11 | currentLanguage: 'en', 12 | }), 13 | })); 14 | 15 | const renderDeathDay = (day: string) => ( 16 | 17 | 18 | 19 | ); 20 | 21 | describe('Common-screens/FamousDetails/DeathDay # en', () => { 22 | const elements = { 23 | deathDay: (api: RenderAPI) => api.queryByTestId('death-day-text'), 24 | }; 25 | 26 | describe('When "day" is "valid"', () => { 27 | it('should render correctly', () => { 28 | const component = render(renderDeathDay('1994-02-21')); 29 | expect(elements.deathDay(component)!.children[0]).toEqual('1994-02-21'); 30 | }); 31 | }); 32 | 33 | describe('When "day" is "invalid"', () => { 34 | it('should render correctly', () => { 35 | const component = render(renderDeathDay('20-122')); 36 | expect(elements.deathDay(component)).toBeNull(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | plugins: [ 4 | ['module:react-native-dotenv'], 5 | ['@babel/plugin-transform-flow-strip-types', { loose: true }], 6 | ['@babel/plugin-proposal-class-properties', { loose: true }], 7 | ['@babel/plugin-transform-private-methods', { loose: true }], 8 | [ 9 | 'module-resolver', 10 | { 11 | root: ['./src'], 12 | extensions: [ 13 | '.ios.ts', 14 | '.android.ts', 15 | '.ts', 16 | '.ios.tsx', 17 | '.android.tsx', 18 | '.tsx', 19 | '.json', 20 | ], 21 | alias: { 22 | '@': ['./src'], 23 | '@styles': './src/styles', 24 | '@app-types': './src/types/index.ts', 25 | '@schema-types': './src/types/schema.ts', 26 | '@hooks': './src/hooks/index.ts', 27 | '@providers': './src/providers/index.ts', 28 | '@common-components': './src/components/common/index.ts', 29 | '@utils': './src/utils/index.ts', 30 | '@navigation': './src/navigation/index.ts', 31 | '@stacks': './src/components/stacks/index.ts', 32 | '@i18n': './src/i18n', 33 | }, 34 | }, 35 | ], 36 | 'react-native-reanimated/plugin', 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/death-day/DeathDay-es.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThemeProvider } from 'styled-components/native'; 3 | import { RenderAPI, render } from '@testing-library/react-native'; 4 | 5 | import { dark as theme } from '@styles/themes'; 6 | 7 | import { DeathDay } from './DeathDay'; 8 | 9 | jest.mock('@hooks', () => ({ 10 | useTranslation: () => ({ 11 | currentLanguage: 'es', 12 | }), 13 | })); 14 | 15 | const renderDeathDay = (day: string) => ( 16 | 17 | 18 | 19 | ); 20 | 21 | describe('Common-screens/FamousDetails/DeathDay # es', () => { 22 | const elements = { 23 | deathDay: (api: RenderAPI) => api.queryByTestId('death-day-text'), 24 | }; 25 | 26 | describe('When "day" is "valid"', () => { 27 | it('should render correctly', () => { 28 | const component = render(renderDeathDay('1994-02-21')); 29 | expect(elements.deathDay(component)!.children[0]).toEqual('21/02/1994'); 30 | }); 31 | }); 32 | 33 | describe('When "day" is "invalid"', () => { 34 | it('should render correctly', () => { 35 | const component = render(renderDeathDay('20/21/122')); 36 | expect(elements.deathDay(component)).toBeNull(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/death-day/DeathDay-pt.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dark as theme } from '@styles/themes'; 4 | 5 | import { DeathDay } from './DeathDay'; 6 | import { ThemeProvider } from 'styled-components/native'; 7 | import { RenderAPI, render } from '@testing-library/react-native'; 8 | 9 | jest.mock('@hooks', () => ({ 10 | useTranslation: () => ({ 11 | currentLanguage: 'pt', 12 | }), 13 | })); 14 | 15 | const renderDeathDay = (day: string) => ( 16 | 17 | 18 | 19 | ); 20 | 21 | describe('Common-screens/FamousDetails/DeathDay # pt', () => { 22 | const elements = { 23 | deathDay: (api: RenderAPI) => api.queryByTestId('death-day-text'), 24 | }; 25 | 26 | describe('When "day" is "valid"', () => { 27 | it('should render correctly', () => { 28 | const component = render(renderDeathDay('1994-02-21')); 29 | expect(elements.deathDay(component)!.children[0]).toEqual('21/02/1994'); 30 | }); 31 | }); 32 | 33 | describe('When "day" is "invalid"', () => { 34 | it('should render correctly', () => { 35 | const component = render(renderDeathDay('20,21,22')); 36 | expect(elements.deathDay(component)).toBeNull(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/questions/components/question-wrapper/QuestionWrapper.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { Typography } from '@common-components'; 4 | import metrics from '@styles/metrics'; 5 | 6 | export const DEFAULT_BORDER_RADIUS = metrics.sm; 7 | export const DEFAULT_CARD_WIDTH = metrics.getWidthFromDP('90'); 8 | 9 | export const Wrapper = styled.View` 10 | width: ${({ theme }) => theme.metrics.width}px; 11 | justify-content: center; 12 | align-items: center; 13 | `; 14 | 15 | export const CardWrapper = styled.View` 16 | width: ${DEFAULT_CARD_WIDTH}px; 17 | border-radius: ${DEFAULT_BORDER_RADIUS}px; 18 | background-color: white; 19 | `; 20 | 21 | export const TextWrapper = styled.View` 22 | align-items: center; 23 | padding: ${({ theme }) => theme.metrics.xl}px; 24 | `; 25 | 26 | export const QuestionsIndicatorText = styled(Typography.ExtraSmallText).attrs( 27 | ({ theme }) => ({ 28 | color: theme.colors.buttonText, 29 | alignment: 'center', 30 | bold: true, 31 | }), 32 | )``; 33 | 34 | export const QuestionText = styled(Typography.ExtraSmallText).attrs( 35 | ({ theme }) => ({ 36 | color: theme.colors.buttonText, 37 | alignment: 'center', 38 | bold: true, 39 | }), 40 | )` 41 | margin-top: ${({ theme }) => theme.metrics.sm}px; 42 | `; 43 | -------------------------------------------------------------------------------- /src/components/common/svg-icon/SVGIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { StyleProp } from 'react-native'; 3 | import { useTheme, Colors } from 'styled-components/native'; 4 | import { SvgXml } from 'react-native-svg'; 5 | 6 | import { FlagsIcons, Icons, flags } from '.'; 7 | import { icons } from './icons'; 8 | 9 | type SupportedIcons = Icons | FlagsIcons; 10 | 11 | export type SVGIconProps = { 12 | color?: keyof Colors; 13 | style?: StyleProp; 14 | id: SupportedIcons; 15 | testID?: string; 16 | size: number; 17 | }; 18 | 19 | const getXML = (id: SupportedIcons, color: string) => { 20 | const XMLIconsMapping: Record = { 21 | ...icons(color), 22 | ...flags, 23 | }; 24 | return XMLIconsMapping[id]; 25 | }; 26 | 27 | export const SVGIcon = (props: SVGIconProps) => { 28 | const theme = useTheme(); 29 | 30 | const xml = useMemo(() => { 31 | if (!props.color) { 32 | return getXML(props.id, theme.colors.text); 33 | } 34 | return getXML(props.id, theme.colors[props.color] as string); 35 | }, [theme, props.color, props.id]); 36 | 37 | return ( 38 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/setup-questions/components/setup-questions-modal/option-list-item/OptionListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { SVGIcon } from '@common-components'; 4 | import metrics from '@/styles/metrics'; 5 | 6 | import * as Styles from './OptionListItem.styles'; 7 | 8 | type OptionListItemProps = { 9 | onPress: () => void; 10 | isSelected: boolean; 11 | title: string; 12 | }; 13 | 14 | const OptionListItem = (props: OptionListItemProps) => ( 15 | 19 | 22 | {props.title} 23 | 24 | {props.isSelected && ( 25 | 30 | )} 31 | 32 | ); 33 | 34 | const shouldComponentUpdate = ( 35 | previousState: OptionListItemProps, 36 | nextState: OptionListItemProps, 37 | ): boolean => 38 | (previousState.isSelected || !nextState.isSelected) && 39 | (!previousState.isSelected || nextState.isSelected); 40 | 41 | export default memo(OptionListItem, shouldComponentUpdate); 42 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/tv-show-details/screen/components/seasons-section/SeasonsSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTheme } from 'styled-components/native'; 3 | 4 | import { Typography } from '@common-components'; 5 | 6 | import { useSeasonsSection } from './use-seasons-section'; 7 | import * as Styles from './SeasonsSection.styles'; 8 | 9 | type SeasonsSectionProps = { 10 | numberOfSeasons: number; 11 | tvShowId?: number | null; 12 | tvShowName?: string | null; 13 | }; 14 | 15 | export const SeasonsSection = (props: SeasonsSectionProps) => { 16 | const seasonsSection = useSeasonsSection({ 17 | numberOfSeasons: props.numberOfSeasons, 18 | tvShowName: props.tvShowName, 19 | tvShowId: props.tvShowId, 20 | }); 21 | const theme = useTheme(); 22 | 23 | return ( 24 | 25 | {seasonsSection.seasons.map((season, index) => ( 26 | seasonsSection.onPress(index + 1)}> 30 | 34 | {season} 35 | 36 | 37 | ))} 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/famous-details/components/death-day/DeathDay.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import { Typography } from '@common-components'; 4 | import { dark } from '@/styles/themes'; 5 | 6 | export const Wrapper = styled.View` 7 | flex-direction: row; 8 | align-items: center; 9 | align-self: flex-start; 10 | margin-left: ${({ theme }) => theme.metrics.md}px; 11 | margin-bottom: ${({ theme }) => theme.metrics.lg * 2}px; 12 | padding-horizontal: ${({ theme }) => theme.metrics.xs}px; 13 | padding-vertical: ${({ theme }) => theme.metrics.xs}px; 14 | border-radius: ${({ theme }) => theme.metrics.height}px; 15 | background-color: ${({ theme }) => theme.colors.primary}; 16 | `; 17 | 18 | export const IconWrapper = styled.View` 19 | width: ${({ theme }) => theme.metrics.getWidthFromDP('8')}px; 20 | height: ${({ theme }) => theme.metrics.getWidthFromDP('8')}px; 21 | justify-content: center; 22 | align-items: center; 23 | border-radius: ${({ theme }) => theme.metrics.lg}px; 24 | margin0-left: ${({ theme }) => theme.metrics.sm}px; 25 | background-color: white; 26 | `; 27 | 28 | export const DateText = styled(Typography.ExtraSmallText).attrs({ 29 | color: dark.colors.buttonText, 30 | alignment: 'center', 31 | bold: true, 32 | })` 33 | margin-horizontal: ${({ theme }) => theme.metrics.sm}px; 34 | `; 35 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/questions/components/multi-choice-question/multi-choice-question-list-item/MultiChoiceQuestionListItem.styles.ts: -------------------------------------------------------------------------------- 1 | import { TouchableOpacityProps } from 'react-native'; 2 | import styled, { IStyledComponent } from 'styled-components/native'; 3 | import { Substitute } from 'styled-components/native/dist/types'; 4 | 5 | type SelectionStyleProps = { 6 | isSelected: boolean; 7 | }; 8 | 9 | export const ListItemWrapper: IStyledComponent< 10 | 'native', 11 | Substitute 12 | > = styled.TouchableOpacity` 13 | flex-direction: row; 14 | align-items: center; 15 | margin-horizontal: ${({ theme }) => theme.metrics.sm}px; 16 | margin-bottom: ${({ theme }) => theme.metrics.md}px; 17 | padding: ${({ theme }) => theme.metrics.md}px; 18 | background-color: ${({ isSelected, theme }) => 19 | isSelected ? theme.colors.primary : theme.colors.white}; 20 | border-width: 1px; 21 | border-color: ${({ isSelected, theme }) => 22 | isSelected ? theme.colors.primary : theme.colors.buttonText}; 23 | border-radius: ${({ theme }) => theme.metrics.width}px; 24 | `; 25 | 26 | export const AnswerTextWrapper = styled.View` 27 | width: 80%; 28 | margin-left: ${({ theme }) => theme.metrics.sm}px; 29 | align-self: center; 30 | padding-horizontal: ${({ theme }) => theme.metrics.md}px; 31 | `; 32 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/results/components/result-list-item/use-result-list-item.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | import { Colors } from 'styled-components/native'; 4 | 5 | import { Icons } from '@common-components'; 6 | import { Translations } from '@i18n/tags'; 7 | import { useTranslation } from '@hooks'; 8 | 9 | import { QuizResult } from '../../use-results'; 10 | 11 | type UseResultListItemParams = { 12 | result: Omit; 13 | }; 14 | 15 | export const useResultListItem = (params: UseResultListItemParams) => { 16 | const translation = useTranslation(); 17 | 18 | const texts = useMemo( 19 | () => ({ 20 | correctAnswer: `${translation.translate( 21 | Translations.Quiz.QUIZ_ANSWER, 22 | )}: ${params.result.answer}`, 23 | userAnswer: `${translation.translate( 24 | Translations.Quiz.QUIZ_YOUR_ANSWER, 25 | )}: ${params.result.userAnswer}`, 26 | }), 27 | [translation.translate], 28 | ); 29 | 30 | const icon = useMemo( 31 | () => ({ 32 | id: params.result.isCorrect 33 | ? ('checkbox-circle' as Icons) 34 | : ('close-circle' as Icons), 35 | colorThemeRef: params.result.isCorrect 36 | ? ('green' as keyof Colors) 37 | : ('red' as keyof Colors), 38 | }), 39 | [params.result.isCorrect], 40 | ); 41 | 42 | return { texts, icon }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/news-loading/NewsLoading.test.tsx: -------------------------------------------------------------------------------- 1 | jest.unmock('react-native-reanimated'); 2 | 3 | import React from 'react'; 4 | import { RenderAPI, cleanup, render } from '@testing-library/react-native'; 5 | import { ThemeProvider } from 'styled-components/native'; 6 | 7 | import { dark as theme } from '@styles/themes/dark'; 8 | 9 | import { NewsLoading, INITIAL_ITEMS_TO_RENDER } from './NewsLoading'; 10 | 11 | const renderNewsLoading = () => ( 12 | 13 | 14 | 15 | ); 16 | 17 | describe('Screens/News/NewsLoading', () => { 18 | afterEach(cleanup); 19 | 20 | const elements = { 21 | newsLoadingList: (api: RenderAPI) => api.queryByTestId('news-loading-list'), 22 | newsListItems: (api: RenderAPI) => 23 | api.queryAllByTestId('news-loading-list-item'), 24 | }; 25 | 26 | it('should render the component correctly', () => { 27 | const component = render(renderNewsLoading()); 28 | expect(elements.newsLoadingList(component)).not.toBeNull(); 29 | expect(elements.newsListItems(component)).not.toBeNull(); 30 | }); 31 | 32 | it('should render the correct number of items', async () => { 33 | const component = render(renderNewsLoading()); 34 | expect(elements.newsListItems(component).length).toEqual( 35 | INITIAL_ITEMS_TO_RENDER, 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/navigation/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform } from 'react-native'; 3 | import { 4 | createBottomTabNavigator, 5 | BottomTabBarProps, 6 | } from '@react-navigation/bottom-tabs'; 7 | 8 | import * as Stacks from '@/components/stacks'; 9 | 10 | import { AndroidNavigationBar } from './AndroidNavigationBar.android'; 11 | import { TabNavigator } from './tab-navigator/TabNavigator'; 12 | import { Routes } from '../routes'; 13 | // import { WRAPPER_HEIGHT } from './tab-navigator/TabNavigator.styles'; 14 | 15 | const Tab = createBottomTabNavigator(); 16 | 17 | const TabBar = (props: BottomTabBarProps) => ; 18 | 19 | export const Tabs = () => ( 20 | <> 21 | 30 | 31 | 32 | 33 | 34 | 35 | {Platform.OS === 'android' && } 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/images-gallery/screen/components/images-list/images-list-item/ImagesListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { TMDBImage } from '@common-components'; 4 | import metrics from '@/styles/metrics'; 5 | 6 | import { useImagesListItem } from './use-images-list-item'; 7 | import * as Styles from './ImagesListItem.styles'; 8 | 9 | type ImagesListItemProps = { 10 | isAllowedToBeShown: boolean; 11 | image: string; 12 | }; 13 | 14 | export const ImagesListItem = memo( 15 | (props: ImagesListItemProps) => { 16 | const imagesListItem = useImagesListItem({ image: props.image }); 17 | 18 | return ( 19 | 20 | 21 | 29 | 30 | 31 | ); 32 | }, 33 | ( 34 | previousState: ImagesListItemProps, 35 | nextState: ImagesListItemProps, 36 | ): boolean => 37 | (previousState.isAllowedToBeShown || !nextState.isAllowedToBeShown) && 38 | (!previousState.isAllowedToBeShown || nextState.isAllowedToBeShown), 39 | ); 40 | -------------------------------------------------------------------------------- /src/components/common/images-list/use-images-list.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useNavigation } from '@react-navigation/native'; 3 | import { StackNavigationProp } from '@react-navigation/stack'; 4 | 5 | import { FamousStackRoutes } from '@/components/stacks/famous/routes/route-params-types'; 6 | import { HomeStackRoutes } from '@/components/stacks/home/routes/route-params-types'; 7 | import { Routes } from '@navigation'; 8 | 9 | type Navigation = StackNavigationProp< 10 | FamousStackRoutes & HomeStackRoutes, 11 | Routes.Famous.IMAGES_GALLERY & Routes.Home.IMAGES_GALLERY 12 | >; 13 | 14 | type UseImagesListParams = { 15 | images: string[]; 16 | }; 17 | 18 | export const useImagesList = (params: UseImagesListParams) => { 19 | const navigation = useNavigation(); 20 | 21 | const handlePressImage = useCallback( 22 | (indexSelected: number) => { 23 | const routeName = navigation.getState().routes[0].name; 24 | const isHomeStack = /HOME/gi.test(routeName); 25 | const imagesGalleryRoute = isHomeStack 26 | ? Routes.Home.IMAGES_GALLERY 27 | : Routes.Famous.IMAGES_GALLERY; 28 | navigation.navigate(imagesGalleryRoute, { 29 | indexSelected, 30 | images: params.images, 31 | }); 32 | }, 33 | [navigation, params.images], 34 | ); 35 | 36 | return { 37 | onPressImage: handlePressImage, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/components/search-bar/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StatusBar } from 'react-native'; 3 | 4 | import { HeaderIconButton } from '@common-components'; 5 | import { dark as theme } from '@styles/themes'; 6 | 7 | import { useSearchBar } from './use-search-bar'; 8 | import * as Styles from './SearchBar.styles'; 9 | 10 | export type SearchBarProps = { 11 | onTypeSearchQuery: (query: string) => void; 12 | onPressClose: () => void; 13 | placeholder: string; 14 | }; 15 | 16 | export const SearchBar = (props: SearchBarProps) => { 17 | const searchBar = useSearchBar(); 18 | 19 | return ( 20 | <> 21 | 26 | 27 | 28 | 34 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/cinetasty/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.cinetasty; 2 | 3 | import android.os.Bundle; 4 | import com.facebook.react.ReactActivity; 5 | import com.facebook.react.ReactActivityDelegate; 6 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 7 | import com.facebook.react.defaults.DefaultReactActivityDelegate; 8 | 9 | public class MainActivity extends ReactActivity { 10 | @Override 11 | protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(null); 13 | } 14 | 15 | /** 16 | * Returns the name of the main component registered from JavaScript. This is used to schedule 17 | * rendering of the component. 18 | */ 19 | @Override 20 | protected String getMainComponentName() { 21 | return "CineTasty"; 22 | } 23 | 24 | /** 25 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link 26 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React 27 | * (aka React 18) with two boolean flags. 28 | */ 29 | @Override 30 | protected ReactActivityDelegate createReactActivityDelegate() { 31 | return new DefaultReactActivityDelegate( 32 | this, 33 | getMainComponentName(), 34 | // If you opted-in for the New Architecture, we enable the Fabric Renderer. 35 | DefaultNewArchitectureEntryPoint.getFabricEnabled()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/common/paginated-list-header/PaginatedListHeader.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RenderAPI, fireEvent, render } from '@testing-library/react-native'; 3 | import { ThemeProvider } from 'styled-components/native'; 4 | 5 | import { dark as theme } from '@styles/themes/dark'; 6 | 7 | import { PaginatedListHeader } from './PaginatedListHeader'; 8 | 9 | const renderPaginationFooter = (onPress = jest.fn()) => ( 10 | 11 | 12 | 13 | ); 14 | 15 | describe('Common-components/PaginatedListHeader', () => { 16 | const elements = { 17 | reloadButton: (api: RenderAPI) => api.getByTestId('top-reload-button'), 18 | icon: (api: RenderAPI) => api.getByTestId('icon-restart'), 19 | }; 20 | 21 | it('should render correctly', () => { 22 | const component = render(renderPaginationFooter()); 23 | expect(elements.reloadButton(component)).not.toBeNull(); 24 | expect(elements.icon(component)).not.toBeNull(); 25 | }); 26 | 27 | it('should call the "onPress" when the user presses the "top-reload-button"', () => { 28 | const onPress = jest.fn(); 29 | const component = render(renderPaginationFooter(onPress)); 30 | expect(onPress).toHaveBeenCalledTimes(0); 31 | fireEvent.press(elements.reloadButton(component)); 32 | expect(onPress).toHaveBeenCalledTimes(1); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/participants-list/participants-list-item/ParticipantsListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { Typography, TMDBImage } from '@common-components'; 4 | 5 | import * as Styles from './ParticipantsListItem.styles'; 6 | 7 | type ParticipantListItemProps = { 8 | onPress: () => void; 9 | subText?: string; 10 | image: string; 11 | name: string; 12 | }; 13 | 14 | export const ParticipantListItem = memo( 15 | (props: ParticipantListItemProps) => ( 16 | 17 | 26 | {/* @ts-ignore */} 27 | 28 | 29 | 30 | {props.name} 31 | 32 | {props.subText && ( 33 | 34 | {props.subText} 35 | 36 | )} 37 | 38 | 39 | ), 40 | () => true, 41 | ); 42 | -------------------------------------------------------------------------------- /src/providers/alert-message/AlertMessage.styles.ts: -------------------------------------------------------------------------------- 1 | import Animated from 'react-native-reanimated'; 2 | import { Platform } from 'react-native'; 3 | import styled, { css } from 'styled-components/native'; 4 | 5 | import { Typography, SVGIcon } from '@/components/common'; 6 | import metrics from '@styles/metrics'; 7 | 8 | export const DEFAULT_HEIGHT = metrics.getWidthFromDP('12%'); 9 | 10 | export const Wrapper = styled(Animated.View)` 11 | height: ${DEFAULT_HEIGHT}px; 12 | flex-direction: row; 13 | justify-content: center; 14 | align-items: center; 15 | align-self: center; 16 | border-radius: ${({ theme }) => theme.borderRadius.xs}px; 17 | background-color: ${({ theme }) => theme.colors.primary}; 18 | padding-horizontal: ${({ theme }) => theme.metrics.md}px; 19 | position: absolute; 20 | `; 21 | 22 | const baseTextMessageStyle = css` 23 | margin-left: ${({ theme }) => theme.metrics.sm}px; 24 | letter-spacing: 0.1px; 25 | `; 26 | 27 | export const Message = styled(Typography.ExtraSmallText).attrs(({ theme }) => ({ 28 | color: theme.colors.buttonText, 29 | bold: true, 30 | }))` 31 | ${Platform.OS === 'android' 32 | ? baseTextMessageStyle 33 | : css` 34 | ${baseTextMessageStyle} 35 | line-height: 0px; 36 | `} 37 | `; 38 | 39 | export const Icon = styled(SVGIcon).attrs(({ theme }) => ({ 40 | size: theme.metrics.getWidthFromDP('6.5%'), 41 | color: 'buttonText', 42 | }))``; 43 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/languages-filter-modal/LanguagesFilterModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ModalSheet } from '@common-components'; 3 | import { NewsLanguage } from '@schema-types'; 4 | 5 | import { LanguageFilterList } from './language-filter-list/LanguageFilterList'; 6 | import { useLanguagesFilterModal } from './use-languages-filter-modal'; 7 | 8 | type LanguagesFilterModalProps = { 9 | onSelectLanguage: (language: NewsLanguage) => void; 10 | languageSelected: NewsLanguage; 11 | onCloseModal: () => void; 12 | isOpen: boolean; 13 | }; 14 | 15 | export const LanguagesFilterModal = (props: LanguagesFilterModalProps) => { 16 | const languagesFilterModal = useLanguagesFilterModal({ 17 | lastLanguageSelected: props.languageSelected, 18 | onSelectLanguage: props.onSelectLanguage, 19 | onCloseModal: props.onCloseModal, 20 | }); 21 | 22 | return ( 23 | 29 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/videos/Videos.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { TVShowDetails_tvShow_videos } from '@schema-types'; 4 | import { SVGIcon } from '@common-components'; 5 | import metrics from '@/styles/metrics'; 6 | 7 | import * as Styles from './Videos.styles'; 8 | import { useVideos } from './use-videos'; 9 | 10 | export type Video = TVShowDetails_tvShow_videos; 11 | 12 | type VideosProps = { 13 | videos: Video[]; 14 | }; 15 | 16 | export const Videos = (props: VideosProps) => { 17 | const videos = useVideos(); 18 | 19 | return ( 20 | 24 | {props.videos.map( 25 | video => 26 | video.thumbnail?.extraSmall && ( 27 | videos.onPress(video.key)} 29 | key={video.key} 30 | testID="video-button"> 31 | 36 | 37 | 38 | 39 | 40 | ), 41 | )} 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/components/recent-searches/RecentSearches.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import { RecentSearchesListItem } from './recent-searchers-list-item/RecentSearchesListItem'; 4 | import * as Styles from './RecentSearches.styles'; 5 | import { SearchItem, SearchType } from '../../types'; 6 | import { useRecentSearches } from './use-recent-searches'; 7 | 8 | type RecentSearchesProps = { 9 | onPressItem: (item: SearchItem) => void; 10 | searchType: SearchType; 11 | }; 12 | 13 | export const RecentSearches = (props: RecentSearchesProps) => { 14 | const recentSearches = useRecentSearches({ 15 | searchType: props.searchType, 16 | }); 17 | 18 | useEffect(() => { 19 | recentSearches.load(); 20 | }, []); 21 | 22 | if (!recentSearches.items.length) { 23 | return null; 24 | } 25 | 26 | return ( 27 | 28 | 29 | {recentSearches.texts.recentSearches} 30 | 31 | {recentSearches.items.map(recentSearch => ( 32 | recentSearches.remove(recentSearch.id ?? -1)} 34 | onPressItem={() => props.onPressItem(recentSearch)} 35 | key={recentSearch.id} 36 | item={recentSearch} 37 | /> 38 | ))} 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/common/media-list-item/MediaListItem.styles.ts: -------------------------------------------------------------------------------- 1 | import { TouchableOpacityProps } from 'react-native'; 2 | import styled, { IStyledComponent } from 'styled-components/native'; 3 | import { Substitute } from 'styled-components/native/dist/types'; 4 | 5 | import metrics from '@styles/metrics'; 6 | 7 | type Measure = { 8 | width: number; 9 | height: number; 10 | }; 11 | 12 | export type LayoutSize = 'large' | 'medium'; 13 | 14 | export const LAYOUT_MEASURES: Record = { 15 | large: { 16 | width: metrics.getWidthFromDP('40'), 17 | height: metrics.getWidthFromDP('60'), 18 | }, 19 | medium: { 20 | width: metrics.getWidthFromDP('30'), 21 | height: metrics.getWidthFromDP('40'), 22 | }, 23 | }; 24 | 25 | type WrapperStyleProps = { 26 | layoutSize: LayoutSize; 27 | }; 28 | 29 | export const Wrapper: IStyledComponent< 30 | 'native', 31 | Substitute 32 | > = styled.TouchableOpacity` 33 | width: ${({ layoutSize }) => LAYOUT_MEASURES[layoutSize].width}px; 34 | height: 100%; 35 | margin-right: ${({ theme }) => theme.metrics.md}px; 36 | `; 37 | 38 | export const StarsContentWrapper = styled.View` 39 | flex-direction: row; 40 | align-items: center; 41 | margin-top: ${({ theme }) => theme.metrics.xs}px; 42 | `; 43 | 44 | export const Gap = styled.View` 45 | width: ${({ theme }) => theme.metrics.xs}px; 46 | height: 1px; 47 | `; 48 | -------------------------------------------------------------------------------- /src/components/stacks/famous/routes/route-params-types.ts: -------------------------------------------------------------------------------- 1 | import { StackNavigationProp } from '@react-navigation/stack'; 2 | 3 | import { Routes } from '@navigation'; 4 | 5 | import { FamousDetailsProps } from '../../common-screens/famous-details/routes/route-params-types'; 6 | import { SearchProps } from '../../common-screens/search/types'; 7 | import { ImagesGalleryNavigationProps } from '../../common-screens/images-gallery/routes/route-params-types'; 8 | import { TVShowDetailsNavigationProps } from '../../common-screens/media-details/tv-show-details/routes/route-params-types'; 9 | import { TVShowSeasonNavigationProps } from '../../common-screens/tv-show-season/routes/route-params-types'; 10 | import { MovieDetailsNavigationProps } from '../../common-screens/media-details/movie-details/routes/route-params-types'; 11 | 12 | export type FamousStackRoutes = { 13 | [Routes.Famous.TRENDING_FAMOUS]: undefined; 14 | [Routes.Famous.DETAILS]: FamousDetailsProps; 15 | [Routes.Famous.SEARCH_FAMOUS]: SearchProps; 16 | [Routes.Famous.TV_SHOW_DETAILS]: TVShowDetailsNavigationProps; 17 | [Routes.Famous.MOVIE_DETAILS]: MovieDetailsNavigationProps; 18 | [Routes.Famous.IMAGES_GALLERY]: ImagesGalleryNavigationProps; 19 | [Routes.Famous.TV_SHOW_SEASON]: TVShowSeasonNavigationProps; 20 | }; 21 | 22 | /** Trending-Famous-Props */ 23 | export type FamousNavigationProp = StackNavigationProp< 24 | FamousStackRoutes, 25 | Routes.Famous.TRENDING_FAMOUS 26 | >; 27 | -------------------------------------------------------------------------------- /src/navigation/components/tab-navigator/tab-navigator-item/TabNavigatorItem.styles.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import styled from 'styled-components/native'; 3 | 4 | import { Typography } from '@common-components'; 5 | import metrics from '@styles/metrics'; 6 | import { dark } from '@styles/themes'; 7 | 8 | import items from '../tabs'; 9 | 10 | export const DEFAULT_ICON_SIZE = metrics.getWidthFromDP('8%'); 11 | 12 | type ItemTextStyleProps = { 13 | isSelected: boolean; 14 | }; 15 | 16 | export const Wrapper = styled.TouchableOpacity` 17 | width: ${({ theme }) => theme.metrics.width / items.length}px; 18 | height: 100%; 19 | align-items: center; 20 | justify-content: center; 21 | `; 22 | 23 | export const ItemText = styled( 24 | Typography.ExtraSmallText, 25 | ).attrs(({ isSelected, theme }) => { 26 | const selectedTabColor = 27 | // Didn't use the theme.id because we would need to 28 | // also check the cases of dark/light when "theme" is "system" 29 | theme.colors.background === dark.colors.background 30 | ? theme.colors.primary 31 | : theme.colors.text; 32 | return { 33 | color: isSelected ? selectedTabColor : theme.colors.inactiveWhite, 34 | }; 35 | })` 36 | margin-top: ${({ theme }) => 37 | Platform.OS === 'android' ? theme.metrics.sm : theme.metrics.xs}px; 38 | font-size: ${({ theme }) => theme.metrics.getWidthFromDP('3.5%')}px; 39 | `; 40 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/media-details/common/background-image/use-background-image.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react'; 2 | import { 3 | useAnimatedStyle, 4 | useSharedValue, 5 | withTiming, 6 | } from 'react-native-reanimated'; 7 | 8 | import { useTMDBImageURI } from '@hooks'; 9 | 10 | import * as Styles from './BackgroundImage.styles'; 11 | 12 | const ANIMATION_DURATION_MS = 500; 13 | 14 | type UseBackgroundImageParams = { 15 | image: string; 16 | }; 17 | 18 | export const useBackgroundImage = (params: UseBackgroundImageParams) => { 19 | const tmdbImage = useTMDBImageURI(); 20 | 21 | const backgroundImageOpacity = useSharedValue(0); 22 | 23 | const style = useAnimatedStyle( 24 | () => ({ 25 | opacity: backgroundImageOpacity.value, 26 | width: Styles.DEFAULT_WIDTH, 27 | height: Styles.DEFAULT_HEIGHT, 28 | position: 'absolute', 29 | }), 30 | [backgroundImageOpacity], 31 | ); 32 | 33 | const handleLoadImage = useCallback(() => { 34 | backgroundImageOpacity.value = withTiming(1, { 35 | duration: ANIMATION_DURATION_MS, 36 | }); 37 | }, []); 38 | 39 | const uri = useMemo( 40 | () => 41 | tmdbImage.uri({ 42 | isThumbnail: true, 43 | imageType: 'backdrop', 44 | image: params.image, 45 | }), 46 | [params.image], 47 | ); 48 | 49 | return { 50 | onLoad: handleLoadImage, 51 | uri, 52 | style, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/questions/components/boolean-question/use-boolean-question.ts: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useCallback } from 'react'; 2 | 3 | import { Translations } from '@i18n/tags'; 4 | import { useTranslation } from '@hooks'; 5 | 6 | type UseBooleanQuestionProps = { 7 | onPressNext: (answerSelected: string) => void; 8 | }; 9 | 10 | export const useBooleanQuestion = (props: UseBooleanQuestionProps) => { 11 | const [selectedAnswer, setSelectedAnswer] = useState(); 12 | 13 | const translation = useTranslation(); 14 | 15 | const handlePressNext = useCallback(() => { 16 | props.onPressNext(String(selectedAnswer)); 17 | }, [selectedAnswer, props.onPressNext]); 18 | 19 | const texts = useMemo( 20 | () => ({ 21 | false: translation.translate(Translations.Quiz.QUIZ_FALSE), 22 | true: translation.translate(Translations.Quiz.QUIZ_TRUE), 23 | next: translation.translate(Translations.Quiz.QUIZ_NEXT), 24 | }), 25 | [translation.translate], 26 | ); 27 | 28 | const handleSelectFalseOption = useCallback(() => { 29 | setSelectedAnswer(false); 30 | }, []); 31 | 32 | const handleSelectTrueOption = useCallback(() => { 33 | setSelectedAnswer(true); 34 | }, []); 35 | 36 | return { 37 | onPressFalseOption: handleSelectFalseOption, 38 | onPressTrueOption: handleSelectTrueOption, 39 | onPressNext: handlePressNext, 40 | selectedAnswer, 41 | texts, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/results/Results.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { FlatList } from 'react-native'; 3 | 4 | import { RoundedButton } from '@common-components'; 5 | import { HeaderTitle } from '@navigation'; 6 | 7 | import { ResultListItem } from './components/result-list-item/ResultListItem'; 8 | import { ResultsProps } from '../../routes/route-params-types'; 9 | import { useResults } from './use-results'; 10 | import * as Styles from './Results.styles'; 11 | 12 | export const Results = (props: ResultsProps) => { 13 | const results = useResults(props); 14 | 15 | useEffect(() => { 16 | props.navigation.setOptions({ 17 | // eslint-disable-next-line react/no-unstable-nested-components 18 | headerTitle: () => , 19 | }); 20 | }, [results.texts.headerText]); 21 | 22 | return ( 23 | 24 | } 26 | keyExtractor={(item, index) => `${item.question}-${index}`} 27 | data={results.quizResults} 28 | contentContainerStyle={Styles.sheet.list} 29 | testID="results-list" 30 | /> 31 | 32 | 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/questions/components/boolean-question/BooleanQuestion.styles.ts: -------------------------------------------------------------------------------- 1 | import { TouchableOpacityProps } from 'react-native'; 2 | import styled, { IStyledComponent } from 'styled-components/native'; 3 | import { Substitute } from 'styled-components/native/dist/types'; 4 | 5 | import { Typography } from '@common-components'; 6 | 7 | type OptionSelectedStyleProps = { 8 | isSelected: boolean; 9 | }; 10 | 11 | export const Wrapper = styled.View` 12 | width: 100%; 13 | padding-horizontal: ${({ theme }) => theme.metrics.xl * 2}px; 14 | flex-direction: row; 15 | justify-content: space-between; 16 | margin-top: ${({ theme }) => theme.metrics.lg}px; 17 | margin-bottom: ${({ theme }) => theme.metrics.xl * 2}px; 18 | `; 19 | 20 | export const OptionButton: IStyledComponent< 21 | 'native', 22 | Substitute 23 | > = styled.TouchableOpacity` 24 | padding-vertical: ${({ theme }) => theme.metrics.lg}px; 25 | padding-horizontal: ${({ theme }) => theme.metrics.xl}px; 26 | border-radius: ${({ theme }) => theme.metrics.sm}px; 27 | background-color: ${({ isSelected, theme }) => 28 | isSelected ? theme.colors.primary : theme.colors.fallbackImageBackground}; 29 | `; 30 | 31 | export const OptionText = styled(Typography.ExtraSmallText).attrs( 32 | ({ theme }) => ({ 33 | color: theme.colors.buttonText, 34 | bold: true, 35 | }), 36 | )` 37 | text-transform: uppercase; 38 | `; 39 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/images-gallery/screen/components/thumbs-list/ThumbsList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList } from 'react-native'; 3 | 4 | import { ThumbsListItem } from './thumbs-list-item/ThumbsListItem'; 5 | import { THUMB_TOTAL_SIZE } from './thumbs-list-item/ThumbsListItem.styles'; 6 | import { useThumbsList } from './use-thumbs-list'; 7 | import * as Styles from './ThumbsList.styles'; 8 | 9 | type ThumbsListProps = { 10 | onPressThumbListItem: (indexThumbSelected: number) => void; 11 | indexImageSelected: number; 12 | thumbs: string[]; 13 | }; 14 | 15 | export const ThumbsList = (props: ThumbsListProps) => { 16 | const thumbsList = useThumbsList({ 17 | indexImageSelected: props.indexImageSelected, 18 | }); 19 | 20 | return ( 21 | ( 23 | props.onPressThumbListItem(index)} 25 | isSelected={props.indexImageSelected === index} 26 | image={item} 27 | /> 28 | )} 29 | getItemLayout={(_, index) => ({ 30 | offset: THUMB_TOTAL_SIZE * index, 31 | length: THUMB_TOTAL_SIZE, 32 | index, 33 | })} 34 | ref={thumbsList.thumbsListRef} 35 | contentContainerStyle={Styles.sheet.list} 36 | showsHorizontalScrollIndicator={false} 37 | keyExtractor={item => item} 38 | data={props.thumbs} 39 | testID="thumbs-list" 40 | horizontal 41 | /> 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/search/components/recent-searches/recent-searchers-list-item/RecentSearchesListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { TMDBImage, SVGIcon } from '@common-components'; 4 | import metrics from '@styles/metrics'; 5 | 6 | import * as Styles from './RecentSearchesListItem.styles'; 7 | import { SearchItem } from '../../../types'; 8 | 9 | type RecentSearchesListItemProps = { 10 | onPressRemove: () => void; 11 | onPressItem: () => void; 12 | item: SearchItem; 13 | }; 14 | 15 | export const RecentSearchesListItem = (props: RecentSearchesListItemProps) => ( 16 | 17 | 20 | <> 21 | 30 | 31 | 32 | {props.item.title || '-'} 33 | 34 | 35 | 38 | 39 | 40 | 41 | ); 42 | -------------------------------------------------------------------------------- /src/providers/theme/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react'; 2 | import { DefaultTheme, ThemeProvider } from 'styled-components/native'; 3 | 4 | import { useAppTheme, INITIAL_THEME } from './use-app-theme'; 5 | 6 | type ThemeContextProps = { 7 | setInitialTheme: () => Promise; 8 | onSetLightTheme: () => void; 9 | onSetSystemTheme: () => void; 10 | onSetDarkTheme: () => void; 11 | theme: DefaultTheme; 12 | }; 13 | 14 | type ThemeContextProviderProps = { 15 | children: JSX.Element; 16 | }; 17 | 18 | export const ThemeContextProvider = (props: ThemeContextProviderProps) => { 19 | const appTheme = useAppTheme(); 20 | 21 | return ( 22 | 30 | 31 | {props.children} 32 | 33 | 34 | ); 35 | }; 36 | 37 | const ThemeContext = createContext({ 38 | setInitialTheme: () => new Promise(resolve => resolve()), 39 | onSetSystemTheme: () => {}, 40 | onSetLightTheme: () => {}, 41 | onSetDarkTheme: () => {}, 42 | theme: INITIAL_THEME, 43 | }); 44 | 45 | export const useThemeProvider = () => useContext(ThemeContext); 46 | 47 | export default ThemeContext; 48 | -------------------------------------------------------------------------------- /src/components/stacks/news/screens/news/components/news-loading/NewsLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { LoadingPlaceholder } from '@common-components'; 4 | import metrics from '@styles/metrics'; 5 | 6 | import * as NewsListItemStyles from '../news-list-item/NewsListItem.styles'; 7 | import { imageWrapper } from '../news-list-item/NewsListItem.styles'; 8 | import * as Styles from './NewsLoading.styles'; 9 | 10 | export const INITIAL_ITEMS_TO_RENDER = Math.floor( 11 | metrics.height / imageWrapper.height, 12 | ); 13 | 14 | const newsLoadingItems = Array(INITIAL_ITEMS_TO_RENDER) 15 | .fill(0) 16 | .map((_, index) => `${index}`); 17 | 18 | export const NewsLoading = () => ( 19 | 20 | {newsLoadingItems.map((newsLoadingItem, index) => ( 21 | 24 | 28 | 29 | 36 | 37 | 38 | 39 | ))} 40 | 41 | ); 42 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/results/components/result-list-item/ResultListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import GLOBAL_STYLES from '@styles/constants'; 4 | import { SVGIcon } from '@common-components'; 5 | import metrics from '@/styles/metrics'; 6 | 7 | import { useResultListItem } from './use-result-list-item'; 8 | import { QuizResult } from '../../use-results'; 9 | import * as Styles from './ResultListItem.styles'; 10 | 11 | type ResultListItemProps = { 12 | result: QuizResult; 13 | }; 14 | 15 | export const ResultListItem = (props: ResultListItemProps) => { 16 | const resultListItem = useResultListItem(props); 17 | 18 | return ( 19 | 22 | 23 | 28 | 29 | 30 | 31 | {props.result.question} 32 | 33 | 34 | {resultListItem.texts.correctAnswer} 35 | 36 | 37 | 38 | {resultListItem.texts.userAnswer} 39 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /__mocks__/utils.ts: -------------------------------------------------------------------------------- 1 | export const scrollFlatListToEnd = { 2 | nativeEvent: { 3 | contentOffset: { 4 | x: 0, 5 | y: 425, 6 | }, 7 | contentSize: { 8 | // Dimensions of the scrollable content 9 | height: 885, 10 | width: 328, 11 | }, 12 | layoutMeasurement: { 13 | // Dimensions of the device 14 | height: 469, 15 | width: 328, 16 | }, 17 | }, 18 | }; 19 | 20 | const getRandomZeroBasedIndex = (array: T[]) => 21 | (Math.random() * (array.length - 1 - 0 + 1)) << 0; 22 | 23 | export const randomPositiveNumber = (max: number, min: number = 0) => 24 | Math.floor(Math.random() * (max - min + 1)) + min; 25 | 26 | export const randomArrayElement = (array: T[], avoid?: number[]) => { 27 | if (!avoid || !avoid.length) { 28 | const randomZeroBasedIndex = getRandomZeroBasedIndex(array); 29 | return array[randomZeroBasedIndex]; 30 | } 31 | while (true) { 32 | const randomZeroBasedIndex = getRandomZeroBasedIndex(array); 33 | if (!avoid.includes(randomZeroBasedIndex)) { 34 | return array[randomZeroBasedIndex]; 35 | } 36 | } 37 | }; 38 | 39 | export const randomArrayIndex = (array: T[], avoid?: number[]) => { 40 | if (!avoid || !avoid.length) { 41 | return getRandomZeroBasedIndex(array); 42 | } 43 | while (true) { 44 | const randomZeroBasedIndex = getRandomZeroBasedIndex(array); 45 | if (!avoid.includes(randomZeroBasedIndex)) { 46 | return randomZeroBasedIndex; 47 | } 48 | } 49 | }; 50 | 51 | export const getErrorType = () => 52 | randomPositiveNumber(1) % 2 === 0 ? 'network' : 'graphql'; 53 | -------------------------------------------------------------------------------- /src/components/common/modal-select-button/ModalSelectButton.styles.ts: -------------------------------------------------------------------------------- 1 | import { TouchableOpacityProps } from 'react-native'; 2 | import styled, { IStyledComponent } from 'styled-components/native'; 3 | import { Substitute } from 'styled-components/native/dist/types'; 4 | 5 | import { isEqualsOrLargerThanIphoneX } from '@utils'; 6 | import { Typography } from '@common-components'; 7 | 8 | interface SelectButtonStyleProps { 9 | borderBottomRightRadius?: number; 10 | borderBottomLeftRadius?: number; 11 | } 12 | 13 | export const SelectButton: IStyledComponent< 14 | 'native', 15 | Substitute 16 | > = styled.TouchableOpacity` 17 | width: 100%; 18 | height: ${({ theme }) => 19 | isEqualsOrLargerThanIphoneX() 20 | ? theme.metrics.getWidthFromDP('20') 21 | : theme.metrics.getWidthFromDP('16')}px; 22 | justify-content: center; 23 | align-items: center; 24 | background-color: ${({ theme }) => theme.colors.primary}; 25 | border-bottom-right-radius: ${({ borderBottomRightRadius }) => 26 | borderBottomRightRadius ?? 0}px; 27 | border-bottom-left-radius: ${({ borderBottomLeftRadius }) => 28 | borderBottomLeftRadius ?? 0}px; 29 | opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; 30 | `; 31 | 32 | export const SelectButtonText = styled(Typography.MediumText).attrs( 33 | ({ theme }) => ({ 34 | color: theme.colors.buttonText, 35 | bold: true, 36 | }), 37 | )` 38 | font-size: ${({ theme }) => theme.metrics.xl}px; 39 | color: ${({ theme }) => theme.colors.buttonText}; 40 | text-transform: uppercase; 41 | `; 42 | -------------------------------------------------------------------------------- /src/components/stacks/common-screens/tv-show-season/screen/components/tv-show-season-loading/TVShowSeasonLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { LoadingPlaceholder } from '@common-components'; 4 | 5 | import * as TVShowSeasonStyles from '../../TVShowSeason.styles'; 6 | import * as Styles from './TVShowSeasonLoading.styles'; 7 | 8 | export const TVShowSeasonLoading = () => ( 9 | <> 10 | 11 | 12 | 13 | 17 | 21 | 25 | 26 | 27 | 28 | {Array(5) 29 | .fill({}) 30 | .map((_, index) => ( 31 | 32 | 40 | 41 | ))} 42 | 43 | 44 | ); 45 | -------------------------------------------------------------------------------- /src/components/stacks/quiz/screens/questions/components/question-wrapper/QuestionWrapper.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThemeProvider } from 'styled-components/native'; 3 | 4 | import { dark as theme } from '@styles/themes'; 5 | 6 | import { QuestionWrapper } from './QuestionWrapper'; 7 | import { View } from 'react-native'; 8 | import { RenderAPI, render } from '@testing-library/react-native'; 9 | 10 | const CURRENT_QUESTION_INDEX = 1; 11 | const NUMBER_OF_QUESTION = 2; 12 | const QUESTION = 'QUESTION'; 13 | 14 | const renderQuestionWrapper = () => ( 15 | 16 | 20 | 21 | 22 | 23 | ); 24 | 25 | describe('Quiz/Questions/MultiChoiceQuestion', () => { 26 | const elements = { 27 | indicatorText: (api: RenderAPI) => 28 | api.getByTestId('question-indicator-text'), 29 | questionText: (api: RenderAPI) => api.getByTestId('question-text'), 30 | children: (api: RenderAPI) => api.getByTestId('children'), 31 | }; 32 | 33 | it('should render the "indicator-text" correctly', () => { 34 | const component = render(renderQuestionWrapper()); 35 | expect(elements.children(component)).not.toBeNull(); 36 | expect(elements.indicatorText(component).children[0]).toEqual( 37 | `${CURRENT_QUESTION_INDEX}/${NUMBER_OF_QUESTION}`, 38 | ); 39 | expect(elements.questionText(component).children[0]).toEqual(QUESTION); 40 | }); 41 | }); 42 | --------------------------------------------------------------------------------