├── .eslintignore ├── .prettierignore ├── src ├── ui │ ├── Home │ │ ├── index.ts │ │ ├── _components │ │ │ ├── Footer.tsx │ │ │ ├── Intro.tsx │ │ │ ├── List.tsx │ │ │ └── Header.tsx │ │ ├── Home.tsx │ │ └── __tests__ │ │ │ └── Home.ispec.tsx │ ├── Detail │ │ ├── index.ts │ │ ├── Detail.tsx │ │ └── __tests__ │ │ │ └── Detail.ispec.tsx │ ├── _utils │ │ └── translations │ │ │ ├── translate.ts │ │ │ └── i18nTranslate.ts │ ├── _navigation │ │ ├── index.ts │ │ ├── hooks.tsx │ │ ├── model.ts │ │ └── StackNavigator.tsx │ ├── _hooks │ │ └── useTranslation.ts │ ├── _di │ │ └── uiModule.ts │ ├── _components │ │ └── Layout.tsx │ ├── _context │ │ └── diContext.tsx │ └── __tests__ │ │ └── render.tsx ├── _di │ ├── resolvers.ts │ ├── types.ts │ ├── container.ts │ └── modules.ts ├── core │ ├── character │ │ ├── domain │ │ │ ├── character.ts │ │ │ ├── characterRepository.ts │ │ │ └── __tests__ │ │ │ │ └── __builders__ │ │ │ │ └── CharacterBuilder.ts │ │ ├── infrastructure │ │ │ ├── apiCharacterRepository.ts │ │ │ └── __tests__ │ │ │ │ └── apiCharacterRepository.spec.ts │ │ ├── useCases │ │ │ ├── characterUseCases.ts │ │ │ └── __tests__ │ │ │ │ └── characterUseCases.spec.ts │ │ └── _di │ │ │ └── characterModule.ts │ ├── comic │ │ ├── domain │ │ │ ├── comic.ts │ │ │ ├── comicRepository.ts │ │ │ └── __tests__ │ │ │ │ └── __builders__ │ │ │ │ └── ComicBuilder.ts │ │ ├── infrastructure │ │ │ ├── apiComicRepository.ts │ │ │ └── __tests__ │ │ │ │ └── apiComicRepository.spec.ts │ │ ├── _di │ │ │ └── comicModule.ts │ │ └── useCases │ │ │ ├── comicUseCases.ts │ │ │ └── __tests__ │ │ │ └── comicUseCases.spec.ts │ └── shared │ │ ├── _di │ │ └── sharedModule.ts │ │ └── api │ │ └── infrastructure │ │ ├── content │ │ ├── characters.json │ │ ├── comics.json │ │ ├── comics-1009664.json │ │ ├── comics-1009610.json │ │ ├── comics-1009718.json │ │ ├── comics-1009368.json │ │ ├── comics-1009351.json │ │ └── comics-1009220.json │ │ └── api.ts └── localizer.ts ├── setupJest.ts ├── assets ├── icon.png ├── favicon.png ├── splash.png ├── adaptive-icon.png └── translations │ └── es.json ├── .husky ├── pre-push └── pre-commit ├── .lintstagedrc ├── .gitignore ├── .prettierrc ├── jest.config.unit.js ├── jest.config.integration.js ├── tsconfig.json ├── babel.config.js ├── app.json ├── App.tsx ├── jest.config.js ├── package.json ├── .eslintrc.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | *.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | -------------------------------------------------------------------------------- /src/ui/Home/index.ts: -------------------------------------------------------------------------------- 1 | export { Home } from './Home' 2 | -------------------------------------------------------------------------------- /src/ui/Detail/index.ts: -------------------------------------------------------------------------------- 1 | export { Detail } from './Detail' 2 | -------------------------------------------------------------------------------- /src/_di/resolvers.ts: -------------------------------------------------------------------------------- 1 | export { asFunction, asValue, asClass } from 'awilix' 2 | -------------------------------------------------------------------------------- /setupJest.ts: -------------------------------------------------------------------------------- 1 | jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper') 2 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/540/react-native-training/main/assets/icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/540/react-native-training/main/assets/favicon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/540/react-native-training/main/assets/splash.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/540/react-native-training/main/assets/adaptive-icon.png -------------------------------------------------------------------------------- /src/core/character/domain/character.ts: -------------------------------------------------------------------------------- 1 | export interface Character { 2 | id: number 3 | name: string 4 | } 5 | -------------------------------------------------------------------------------- /src/ui/_utils/translations/translate.ts: -------------------------------------------------------------------------------- 1 | export type Translate = (keys: string | string[], options?: object) => string 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | echo 'Pre-push: Running tests...' 5 | yarn test 6 | 7 | -------------------------------------------------------------------------------- /src/ui/_navigation/index.ts: -------------------------------------------------------------------------------- 1 | export { useNavigation, useRoute } from './hooks' 2 | export { NavigationParams } from './model' 3 | -------------------------------------------------------------------------------- /src/core/comic/domain/comic.ts: -------------------------------------------------------------------------------- 1 | export interface Comic { 2 | id: number 3 | title: string 4 | characters: string[] 5 | } 6 | -------------------------------------------------------------------------------- /src/ui/_hooks/useTranslation.ts: -------------------------------------------------------------------------------- 1 | import { useContainer } from 'ui/_context/diContext' 2 | 3 | export const useTranslation = () => ({ 4 | t: useContainer().translate 5 | }) 6 | -------------------------------------------------------------------------------- /src/core/comic/domain/comicRepository.ts: -------------------------------------------------------------------------------- 1 | import { Comic } from './comic' 2 | 3 | export interface ComicRepository { 4 | findBy: (characterId: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/core/character/domain/characterRepository.ts: -------------------------------------------------------------------------------- 1 | import { Character } from './character' 2 | 3 | export interface CharacterRepository { 4 | all: () => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/ui/_utils/translations/i18nTranslate.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next' 2 | import { Translate } from './translate' 3 | 4 | export const i18nTranslate = i18next.t satisfies Translate 5 | -------------------------------------------------------------------------------- /src/_di/types.ts: -------------------------------------------------------------------------------- 1 | import { modules } from './modules' 2 | 3 | export type Module = typeof modules 4 | 5 | export type Container = { 6 | [P in keyof Module]: ReturnType 7 | } 8 | -------------------------------------------------------------------------------- /src/core/shared/_di/sharedModule.ts: -------------------------------------------------------------------------------- 1 | import { api, Api } from '../api/infrastructure/api' 2 | import { asValue } from '_di/resolvers' 3 | 4 | export const sharedModule = { 5 | api: asValue(api(3)) 6 | } 7 | -------------------------------------------------------------------------------- /src/_di/container.ts: -------------------------------------------------------------------------------- 1 | import { createContainer, InjectionMode } from 'awilix' 2 | import { Container } from './types' 3 | 4 | export const container = createContainer({ 5 | injectionMode: InjectionMode.PROXY 6 | }) 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{ts,tsx}": [ 3 | "prettier --write", 4 | "eslint --ext=js,jsx,ts,tsx --fix" 5 | ], 6 | "src/**/*.{js,json,md}": [ 7 | "prettier --write", 8 | "eslint --fix" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # IntelliJ 17 | .idea 18 | 19 | .jest/ 20 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "jsxBracketSameLine": true, 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid", 10 | "printWidth": 120 11 | } 12 | -------------------------------------------------------------------------------- /src/ui/_navigation/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { RouteName } from './model' 2 | import { useContainer } from 'ui/_context/diContext' 3 | 4 | export const useNavigation = () => useContainer().useNavigation() 5 | 6 | export const useRoute = () => useContainer().useRoute() 7 | -------------------------------------------------------------------------------- /jest.config.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Readonly file. Add config in jest.config.js 4 | var config = require('./jest.config') 5 | config.testRegex = '\\.spec\\.(ts|js)$' 6 | //eslint-disable-next-line no-console 7 | console.log('RUNNING UNIT TESTS') 8 | 9 | module.exports = config 10 | -------------------------------------------------------------------------------- /jest.config.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Readonly file. Add config in jest.config.js 4 | var config = require('./jest.config') 5 | config.testRegex = '\\.ispec\\.(ts|tsx)$' 6 | //eslint-disable-next-line no-console 7 | console.log('RUNNING INTEGRATION TESTS') 8 | 9 | module.exports = config 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | branch="$(git rev-parse --abbrev-ref HEAD)" 5 | 6 | if [ "$branch" = "main" ]; then 7 | echo "You can't commit directly to main branch" 8 | exit 1 9 | fi 10 | 11 | yarn tsc --project tsconfig.json --noEmit --skipLibCheck 12 | yarn lint-staged -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "core/*": [ 6 | "src/core/*" 7 | ], 8 | "ui/*": [ 9 | "src/ui/*" 10 | ], 11 | "_di/*": [ 12 | "src/_di/*" 13 | ] 14 | } 15 | }, 16 | "extends": "expo/tsconfig.base", 17 | } 18 | -------------------------------------------------------------------------------- /src/core/comic/infrastructure/apiComicRepository.ts: -------------------------------------------------------------------------------- 1 | import { ComicRepository } from '../domain/ComicRepository' 2 | import { Api } from 'core/shared/api/infrastructure/api' 3 | 4 | export const apiComicRepository = ({ api }: { api: Api }) => 5 | ({ 6 | findBy: (characterId: string) => api.comics(characterId) 7 | } satisfies ComicRepository) 8 | -------------------------------------------------------------------------------- /src/ui/Home/_components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native' 2 | import { useTranslation } from 'ui/_hooks/useTranslation' 3 | 4 | interface Props { 5 | comicCount: number 6 | } 7 | 8 | export const Footer = ({ comicCount }: Props) => { 9 | const { t } = useTranslation() 10 | 11 | return {t('home.comic.count', { comicCount })} 12 | } 13 | -------------------------------------------------------------------------------- /src/_di/modules.ts: -------------------------------------------------------------------------------- 1 | import { characterModule } from 'core/character/_di/characterModule' 2 | import { comicModule } from 'core/comic/_di/comicModule' 3 | import { sharedModule } from 'core/shared/_di/sharedModule' 4 | import { uiModule } from 'ui/_di/uiModule' 5 | 6 | export const modules = { 7 | ...comicModule, 8 | ...characterModule, 9 | ...sharedModule, 10 | ...uiModule 11 | } 12 | -------------------------------------------------------------------------------- /src/ui/_navigation/model.ts: -------------------------------------------------------------------------------- 1 | import { Comic } from 'core/comic/domain/comic' 2 | import { NativeStackNavigationProp } from '@react-navigation/native-stack' 3 | 4 | export type NavigationParams = { 5 | Home: undefined 6 | Detail: { comic: Comic } 7 | } 8 | 9 | export type RouteName = keyof NavigationParams 10 | 11 | export type Navigation = () => NativeStackNavigationProp 12 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | [ 7 | 'module-resolver', 8 | { 9 | alias: { 10 | core: './src/core', 11 | ui: './src/ui', 12 | _di: './src/_di' 13 | } 14 | } 15 | ] 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/character/infrastructure/apiCharacterRepository.ts: -------------------------------------------------------------------------------- 1 | import { CharacterRepository } from '../domain/CharacterRepository' 2 | import { Api } from 'core/shared/api/infrastructure/api' 3 | 4 | export class ApiCharacterRepository implements CharacterRepository { 5 | private api: Api 6 | 7 | constructor({ api }: { api: Api }) { 8 | this.api = api 9 | } 10 | 11 | all = () => this.api.characters() 12 | } 13 | -------------------------------------------------------------------------------- /src/core/character/useCases/characterUseCases.ts: -------------------------------------------------------------------------------- 1 | import { Character } from '../domain/character' 2 | import { CharacterRepository } from '../domain/CharacterRepository' 3 | 4 | export interface CharacterUseCases { 5 | all: () => Promise 6 | } 7 | 8 | export const characterUseCases = ({ characterRepository }: { characterRepository: CharacterRepository }) => 9 | ({ 10 | all: () => characterRepository.all() 11 | } satisfies CharacterUseCases) 12 | -------------------------------------------------------------------------------- /src/core/comic/_di/comicModule.ts: -------------------------------------------------------------------------------- 1 | import { ComicUseCases, comicUseCases } from '../useCases/comicUseCases' 2 | import { ComicRepository } from '../domain/comicRepository' 3 | import { apiComicRepository } from '../infrastructure/apiComicRepository' 4 | import { asFunction } from '_di/resolvers' 5 | 6 | export const comicModule = { 7 | comicUseCases: asFunction(comicUseCases), 8 | comicRepository: asFunction(apiComicRepository) 9 | } 10 | -------------------------------------------------------------------------------- /src/core/shared/api/infrastructure/content/characters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1009220, 4 | "name": "Captain America" 5 | }, 6 | { 7 | "id": 1009351, 8 | "name": "Hulk" 9 | }, 10 | { 11 | "id": 1009368, 12 | "name": "Iron Man" 13 | }, 14 | { 15 | "id": 1009610, 16 | "name": "Spider-Man" 17 | }, 18 | { 19 | "id": 1009664, 20 | "name": "Thor" 21 | }, 22 | { 23 | "id": 1009718, 24 | "name": "Wolverine" 25 | } 26 | ] -------------------------------------------------------------------------------- /src/ui/_navigation/StackNavigator.tsx: -------------------------------------------------------------------------------- 1 | import { createNativeStackNavigator } from '@react-navigation/native-stack' 2 | import { Detail } from 'ui/Detail' 3 | import { Home } from 'ui/Home' 4 | import { NavigationParams } from 'ui/_navigation/model' 5 | 6 | const Stack = createNativeStackNavigator() 7 | 8 | export const StackNavigator = () => ( 9 | 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /src/core/character/_di/characterModule.ts: -------------------------------------------------------------------------------- 1 | import { ApiCharacterRepository } from '../infrastructure/apiCharacterRepository' 2 | import { CharacterRepository } from '../domain/CharacterRepository' 3 | import { characterUseCases, CharacterUseCases } from '../useCases/characterUseCases' 4 | import { asClass, asFunction } from '_di/resolvers' 5 | 6 | export const characterModule = { 7 | characterUseCases: asFunction(characterUseCases), 8 | characterRepository: asClass(ApiCharacterRepository) 9 | } 10 | -------------------------------------------------------------------------------- /src/ui/_di/uiModule.ts: -------------------------------------------------------------------------------- 1 | import { asValue } from '_di/resolvers' 2 | import { Translate } from 'ui/_utils/translations/translate' 3 | import { i18nTranslate } from 'ui/_utils/translations/i18nTranslate' 4 | import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' 5 | import { Navigation, NavigationParams, RouteName } from 'ui/_navigation/model' 6 | 7 | export const uiModule = { 8 | translate: asValue(i18nTranslate), 9 | useNavigation: asValue(useNavigation), 10 | useRoute: asValue(() => useRoute>()) 11 | } 12 | -------------------------------------------------------------------------------- /src/localizer.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import * as Localization from 'expo-localization' 4 | import translationES from '../assets/translations/es.json' 5 | 6 | export const localizer = { 7 | init: () => 8 | i18n.use(initReactI18next).init({ 9 | compatibilityJSON: 'v3', 10 | lng: Localization.locale, 11 | fallbackLng: 'es', 12 | interpolation: { 13 | escapeValue: false 14 | }, 15 | resources: { 16 | es: { 17 | translation: translationES 18 | } 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/core/shared/api/infrastructure/content/comics.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 45977, 4 | "title": "Captain America (2012) #11", 5 | "characters": ["Captain America"] 6 | }, 7 | { 8 | "id": 43722, 9 | "title": "Captain America (2012) #1", 10 | "characters": ["Captain America"] 11 | }, 12 | { 13 | "id": 40391, 14 | "title": "Captain America (2011) #18", 15 | "characters": ["Captain America"] 16 | }, 17 | { 18 | "id": 43339, 19 | "title": "Uncanny Avengers (2012) #1", 20 | "characters": ["Captain America", "Havok", "Rogue", "Scarlet Witch", "Thor", "Wolverine"] 21 | } 22 | ] -------------------------------------------------------------------------------- /src/ui/_components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { SafeAreaView, StyleSheet, View } from 'react-native' 3 | 4 | interface Props { 5 | children: ReactNode 6 | } 7 | 8 | export const Layout = ({ children }: Props) => ( 9 | 10 | {children} 11 | 12 | ) 13 | 14 | const styles = StyleSheet.create({ 15 | container: { 16 | flex: 1, 17 | backgroundColor: '#fff' 18 | }, 19 | wrapper: { 20 | flex: 1, 21 | justifyContent: 'flex-start', 22 | paddingHorizontal: 10, 23 | paddingVertical: 20 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /src/ui/_context/diContext.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from '_di/types' 2 | import { createContext, ReactNode, useContext, useRef } from 'react' 3 | import { container } from '_di/container' 4 | 5 | const emptyContainer = container.register({}) 6 | 7 | export const DiContext = createContext(emptyContainer.cradle) 8 | 9 | export const DiProvider = ({ children, container }: { children: ReactNode; container: Container }) => { 10 | const containerRef = useRef(container) 11 | 12 | return {children} 13 | } 14 | 15 | export const useContainer = () => useContext(DiContext) 16 | -------------------------------------------------------------------------------- /src/core/comic/infrastructure/__tests__/apiComicRepository.spec.ts: -------------------------------------------------------------------------------- 1 | import { mock } from 'jest-mock-extended' 2 | import { Api } from 'core/shared/api/infrastructure/api' 3 | import { aComic } from '../../domain/__tests__/__builders__/ComicBuilder' 4 | import { apiComicRepository } from '../apiComicRepository' 5 | 6 | describe('Comic repository', () => { 7 | it('returns a comic', async () => { 8 | const api = mock() 9 | api.comics.mockResolvedValue(aComic()) 10 | 11 | const comic = await apiComicRepository({ api }).findBy('1') 12 | 13 | expect(api.comics).toHaveBeenCalledWith('1') 14 | expect(comic).toEqual(aComic()) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/ui/Home/_components/Intro.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Text } from 'react-native' 2 | import { useTranslation } from 'ui/_hooks/useTranslation' 3 | 4 | export const Intro = () => { 5 | const { t } = useTranslation() 6 | 7 | return ( 8 | <> 9 | {t('home.title')} 10 | {t('home.description')} 11 | 12 | ) 13 | } 14 | 15 | const styles = StyleSheet.create({ 16 | title: { 17 | fontWeight: 'bold', 18 | fontSize: 30, 19 | marginBottom: 20, 20 | textAlign: 'left' 21 | }, 22 | description: { 23 | marginBottom: 30, 24 | textAlign: 'left' 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /src/core/character/domain/__tests__/__builders__/CharacterBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Character } from '../../character' 2 | 3 | export const aCharacter = (...options: Array>) => { 4 | const _default: Character = { 5 | id: 1, 6 | name: 'character' 7 | } 8 | 9 | return Object.assign({}, { ..._default, ...options }) 10 | } 11 | 12 | export const aCharacterCollection = (items = 4, ...options: Array>) => { 13 | const _default: Character[] = [] 14 | 15 | for (let i = 0; i < items; i++) { 16 | _default.push({ 17 | id: i, 18 | name: `character ${i}`, 19 | ...options 20 | }) 21 | } 22 | 23 | return _default 24 | } 25 | -------------------------------------------------------------------------------- /src/core/comic/domain/__tests__/__builders__/ComicBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Comic } from '../../comic' 2 | 3 | export const aComic = (...options: Array>) => { 4 | const _default: Comic = { 5 | id: 1, 6 | title: 'title', 7 | characters: ['characters'] 8 | } 9 | 10 | return Object.assign({}, _default, ...options) 11 | } 12 | 13 | export const aComicCollection = (size = 4, options?: Comic) => { 14 | const _default: Comic[] = [] 15 | 16 | for (let i = 0; i < size; i++) { 17 | _default.push({ 18 | id: i, 19 | title: `title ${i}`, 20 | characters: [`characters ${i}`] 21 | }) 22 | } 23 | 24 | options && _default.push(options) 25 | 26 | return _default 27 | } 28 | -------------------------------------------------------------------------------- /assets/translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "title": "Buscador de cómics de Marvel", 4 | "description": "Este buscador encontrará los cómics en los que aparezcan los dos personajes que selecciones en el formulario", 5 | "selector":{ 6 | "title": "Selecciona una pareja de personajes:", 7 | "placeholder": "Selecciona un personaje..." 8 | }, 9 | "button": { 10 | "reset":"Limpiar" 11 | }, 12 | "comic": { 13 | "count": "Elementos en la lista: {{comicCount}}", 14 | "detail": "Ver detalle" 15 | } 16 | }, 17 | "detail": { 18 | "count": "Elementos en la lista: {{charactersCount}}", 19 | "button": { 20 | "back": "Volver a la lista de comics" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-training", 4 | "slug": "react-native-training", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/adaptive-icon.png", 26 | "backgroundColor": "#FFFFFF" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationContainer } from '@react-navigation/native' 2 | import { QueryClient, QueryClientProvider } from 'react-query' 3 | import { localizer } from './src/localizer' 4 | import { DiProvider } from 'ui/_context/diContext' 5 | import { StackNavigator } from 'ui/_navigation/StackNavigator' 6 | import { modules } from '_di/modules' 7 | import { container } from '_di/container' 8 | 9 | localizer.init() 10 | const queryClient = new QueryClient() 11 | const diContainer = container.register({ ...modules }) 12 | 13 | const App = () => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | 23 | // eslint-disable-next-line import/no-default-export 24 | export default App 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.TZ = 'UTC' 4 | 5 | module.exports = { 6 | preset: 'jest-expo', 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], 8 | moduleNameMapper: { 9 | '^_di/(.*)$': '/src/_di/$1', 10 | '^core/(.*)$': '/src/core/$1', 11 | '^ui/(.*)$': '/src/ui/$1' 12 | }, 13 | transformIgnorePatterns: [ 14 | 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|@gorhom/bottom-sheet|toggle-switch-react-native|@sentry/.*|sentry-expo)' 15 | ], 16 | cacheDirectory: '.jest/cache', 17 | resetMocks: true, 18 | testRegex: '\\.spec|.ispec\\.(ts|tsx)$', 19 | setupFilesAfterEnv: ['./setupJest.ts', '@testing-library/jest-native/extend-expect'], 20 | reporters: ['default', 'github-actions'] 21 | } 22 | -------------------------------------------------------------------------------- /src/core/character/infrastructure/__tests__/apiCharacterRepository.spec.ts: -------------------------------------------------------------------------------- 1 | import { aCharacterCollection } from '../../domain/__tests__/__builders__/CharacterBuilder' 2 | import { mock } from 'jest-mock-extended' 3 | import { Api } from 'core/shared/api/infrastructure/api' 4 | import { ApiCharacterRepository } from '../apiCharacterRepository' 5 | 6 | describe('Character repository', () => { 7 | it('returns all characters', async () => { 8 | const api = mock() 9 | api.characters.mockResolvedValue(aCharacterCollection()) 10 | 11 | const characters = await new ApiCharacterRepository({ api }).all() 12 | 13 | expect(characters).toEqual(aCharacterCollection()) 14 | }) 15 | 16 | it('returns no characters', async () => { 17 | const api = mock() 18 | api.characters.mockResolvedValue([]) 19 | 20 | const characters = await new ApiCharacterRepository({ api }).all() 21 | 22 | expect(characters).toHaveLength(0) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/core/comic/useCases/comicUseCases.ts: -------------------------------------------------------------------------------- 1 | import { ComicRepository } from '../domain/ComicRepository' 2 | import { Comic } from '../domain/comic' 3 | 4 | export interface ComicUseCases { 5 | commonComics: (firstCharacterFilter?: string, secondCharacterFilter?: string) => Promise 6 | } 7 | 8 | export const comicUseCases = ({ comicRepository }: { comicRepository: ComicRepository }) => 9 | ({ 10 | commonComics: async (firstCharacterFilter?: string, secondCharacterFilter?: string) => { 11 | if (firstCharacterFilter === undefined || secondCharacterFilter === undefined) { 12 | return [] 13 | } 14 | 15 | const [firstCharacterComics, secondCharacterComics] = await Promise.all([ 16 | comicRepository.findBy(firstCharacterFilter), 17 | comicRepository.findBy(secondCharacterFilter) 18 | ]) 19 | 20 | return firstCharacterComics.filter(comic1 => secondCharacterComics.some(comic2 => comic1.id === comic2.id)) 21 | } 22 | } satisfies ComicUseCases) 23 | -------------------------------------------------------------------------------- /src/core/character/useCases/__tests__/characterUseCases.spec.ts: -------------------------------------------------------------------------------- 1 | import { CharacterRepository } from '../../domain/characterRepository' 2 | import { aCharacterCollection } from '../../domain/__tests__/__builders__/CharacterBuilder' 3 | import { mock } from 'jest-mock-extended' 4 | import { characterUseCases } from '../characterUseCases' 5 | 6 | describe('Character use case', () => { 7 | it('returns all characters', async () => { 8 | const characterRepository = mock() 9 | characterRepository.all.mockResolvedValue(aCharacterCollection()) 10 | 11 | const characters = await characterUseCases({ characterRepository }).all() 12 | 13 | expect(characters).toEqual(aCharacterCollection()) 14 | }) 15 | 16 | it('returns no characters', async () => { 17 | const characterRepository = mock() 18 | characterRepository.all.mockResolvedValue([]) 19 | 20 | const characters = await characterUseCases({ characterRepository }).all() 21 | 22 | expect(characters).toHaveLength(0) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/ui/Detail/Detail.tsx: -------------------------------------------------------------------------------- 1 | import { Button, StyleSheet, Text, View } from 'react-native' 2 | import { Layout } from 'ui/_components/Layout' 3 | import { useRoute, useNavigation } from 'ui/_navigation' 4 | import { useTranslation } from 'ui/_hooks/useTranslation' 5 | 6 | export const Detail = () => { 7 | const { 8 | params: { comic } 9 | } = useRoute<'Detail'>() 10 | const navigation = useNavigation() 11 | const { t } = useTranslation() 12 | 13 | return ( 14 | 15 | {comic.title} 16 | {comic.characters.map(character => ( 17 | 18 | {character} 19 | 20 | ))} 21 | 22 | {t('detail.count', { charactersCount: comic.characters.length })} 23 |