├── .dockerignore ├── .gitignore ├── Dockerfile ├── assets └── heroes │ ├── dc-arrow.jpg │ ├── dc-batman.jpg │ ├── dc-black.jpg │ ├── dc-blue.jpg │ ├── dc-flash.jpg │ ├── dc-green.jpg │ ├── dc-martian.jpg │ ├── dc-robin.jpg │ ├── dc-superman.jpg │ ├── dc-wonder.jpg │ ├── marvel-captain.jpg │ ├── marvel-cyclops.jpg │ ├── marvel-daredevil.jpg │ ├── marvel-hawkeye.jpg │ ├── marvel-hulk.jpg │ ├── marvel-iron.jpg │ ├── marvel-silver.jpg │ ├── marvel-spider.jpg │ ├── marvel-thor.jpg │ └── marvel-wolverine.jpg ├── babel.config.js ├── index.html ├── jest.config.js ├── nginx └── nginx.conf ├── package.json ├── src ├── HeroesApp.jsx ├── auth │ ├── context │ │ ├── AuthContext.jsx │ │ ├── AuthProvider.jsx │ │ ├── authReducer.js │ │ └── index.js │ ├── index.js │ ├── pages │ │ ├── LoginPage.jsx │ │ └── index.js │ └── types │ │ └── types.js ├── heroes │ ├── components │ │ ├── HeroCard.jsx │ │ ├── HeroList.jsx │ │ └── index.js │ ├── data │ │ └── heroes.js │ ├── helpers │ │ ├── getHeroById.js │ │ ├── getHeroesByName.js │ │ ├── getHeroesByPublisher.js │ │ └── index.js │ ├── index.js │ ├── pages │ │ ├── DcPage.jsx │ │ ├── HeroPage.jsx │ │ ├── MarvelPage.jsx │ │ ├── SearchPage.jsx │ │ └── index.js │ └── routes │ │ └── HeroesRoutes.jsx ├── hooks │ └── useForm.js ├── main.jsx ├── router │ ├── AppRouter.jsx │ ├── PrivateRoute.jsx │ └── PublicRoute.jsx ├── styles.css └── ui │ ├── components │ ├── Navbar.jsx │ └── index.js │ └── index.js ├── tests ├── auth │ ├── context │ │ └── authReducer.test.js │ └── types │ │ └── types.test.js ├── heroes │ └── pages │ │ ├── SearchPage.test.jsx │ │ └── __snapshots__ │ │ └── SearchPage.test.jsx.snap ├── router │ ├── AppRouter.test.jsx │ ├── PrivateRoute.test.jsx │ └── PublicRoute.test.jsx └── ui │ └── components │ └── Navbar.test.jsx ├── vite.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | node_modules 4 | .git 5 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:19-alpine3.15 as dev-deps 3 | WORKDIR /app 4 | COPY package.json package.json 5 | RUN yarn install --frozen-lockfile 6 | 7 | 8 | FROM node:19-alpine3.15 as builder 9 | WORKDIR /app 10 | COPY --from=dev-deps /app/node_modules ./node_modules 11 | COPY . . 12 | # RUN yarn test 13 | RUN yarn build 14 | 15 | 16 | FROM nginx:1.23.3 as prod 17 | EXPOSE 80 18 | 19 | COPY --from=builder /app/dist /usr/share/nginx/html 20 | COPY assets/ /usr/share/nginx/html/assets 21 | RUN rm /etc/nginx/conf.d/default.conf 22 | COPY nginx/nginx.conf /etc/nginx/conf.d 23 | 24 | CMD [ "nginx","-g", "daemon off;" ] 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /assets/heroes/dc-arrow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/dc-arrow.jpg -------------------------------------------------------------------------------- /assets/heroes/dc-batman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/dc-batman.jpg -------------------------------------------------------------------------------- /assets/heroes/dc-black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/dc-black.jpg -------------------------------------------------------------------------------- /assets/heroes/dc-blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/dc-blue.jpg -------------------------------------------------------------------------------- /assets/heroes/dc-flash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/dc-flash.jpg -------------------------------------------------------------------------------- /assets/heroes/dc-green.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/dc-green.jpg -------------------------------------------------------------------------------- /assets/heroes/dc-martian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/dc-martian.jpg -------------------------------------------------------------------------------- /assets/heroes/dc-robin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/dc-robin.jpg -------------------------------------------------------------------------------- /assets/heroes/dc-superman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/dc-superman.jpg -------------------------------------------------------------------------------- /assets/heroes/dc-wonder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/dc-wonder.jpg -------------------------------------------------------------------------------- /assets/heroes/marvel-captain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/marvel-captain.jpg -------------------------------------------------------------------------------- /assets/heroes/marvel-cyclops.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/marvel-cyclops.jpg -------------------------------------------------------------------------------- /assets/heroes/marvel-daredevil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/marvel-daredevil.jpg -------------------------------------------------------------------------------- /assets/heroes/marvel-hawkeye.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/marvel-hawkeye.jpg -------------------------------------------------------------------------------- /assets/heroes/marvel-hulk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/marvel-hulk.jpg -------------------------------------------------------------------------------- /assets/heroes/marvel-iron.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/marvel-iron.jpg -------------------------------------------------------------------------------- /assets/heroes/marvel-silver.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/marvel-silver.jpg -------------------------------------------------------------------------------- /assets/heroes/marvel-spider.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/marvel-spider.jpg -------------------------------------------------------------------------------- /assets/heroes/marvel-thor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/marvel-thor.jpg -------------------------------------------------------------------------------- /assets/heroes/marvel-wolverine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/assets/heroes/marvel-wolverine.jpg -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ '@babel/preset-env', { targets: { esmodules: true } } ], 4 | [ '@babel/preset-react', { runtime: 'automatic' } ], 5 | ], 6 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jest-environment-jsdom', 3 | // setupFiles: ['./jest.setup.js'] 4 | } -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | server_name localhost; 5 | 6 | #access_log /var/log/nginx/host.access.log main; 7 | 8 | location / { 9 | root /usr/share/nginx/html; 10 | index index.html index.htm; 11 | try_files $uri $uri/ /index.html; 12 | } 13 | 14 | #error_page 404 /404.html; 15 | 16 | # redirect server error pages to the static page /50x.html 17 | # 18 | error_page 500 502 503 504 /50x.html; 19 | location = /50x.html { 20 | root /usr/share/nginx/html; 21 | } 22 | 23 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 24 | # 25 | #location ~ \.php$ { 26 | # proxy_pass http://127.0.0.1; 27 | #} 28 | 29 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 30 | # 31 | #location ~ \.php$ { 32 | # root html; 33 | # fastcgi_pass 127.0.0.1:9000; 34 | # fastcgi_index index.php; 35 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 36 | # include fastcgi_params; 37 | #} 38 | 39 | # deny access to .htaccess files, if Apache's document root 40 | # concurs with nginx's one 41 | # 42 | #location ~ /\.ht { 43 | # deny all; 44 | #} 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heroes-spa", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "test": "jest --watchAll" 10 | }, 11 | "dependencies": { 12 | "query-string": "^7.1.1", 13 | "react": "^18.0.0", 14 | "react-dom": "^18.0.0", 15 | "react-router-dom": "6" 16 | }, 17 | "devDependencies": { 18 | "@babel/preset-env": "^7.17.10", 19 | "@babel/preset-react": "^7.16.7", 20 | "@testing-library/react": "^13.2.0", 21 | "@types/jest": "^27.5.0", 22 | "@types/react": "^18.0.0", 23 | "@types/react-dom": "^18.0.0", 24 | "@vitejs/plugin-react": "^1.3.0", 25 | "babel-jest": "^28.1.0", 26 | "jest": "^28.1.0", 27 | "jest-environment-jsdom": "^28.1.0", 28 | "vite": "^2.9.7" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/HeroesApp.jsx: -------------------------------------------------------------------------------- 1 | import { AuthProvider } from './auth'; 2 | import { AppRouter } from './router/AppRouter'; 3 | 4 | 5 | export const HeroesApp = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/auth/context/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | 4 | export const AuthContext = createContext(); -------------------------------------------------------------------------------- /src/auth/context/AuthProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react'; 2 | import { AuthContext } from './AuthContext'; 3 | import { authReducer } from './authReducer'; 4 | 5 | import { types } from '../types/types'; 6 | 7 | // const initialState = { 8 | // logged: false, 9 | // } 10 | 11 | const init = () => { 12 | const user = JSON.parse( localStorage.getItem('user') ); 13 | 14 | return { 15 | logged: !!user, 16 | user: user, 17 | } 18 | } 19 | 20 | 21 | export const AuthProvider = ({ children }) => { 22 | 23 | const [ authState, dispatch ] = useReducer( authReducer, {}, init ); 24 | 25 | const login = ( name = '' ) => { 26 | 27 | const user = { id: 'ABC', name } 28 | const action = { type: types.login, payload: user } 29 | 30 | localStorage.setItem('user', JSON.stringify( user ) ); 31 | 32 | dispatch(action); 33 | } 34 | 35 | const logout = () => { 36 | localStorage.removeItem('user'); 37 | const action = { type: types.logout }; 38 | dispatch(action); 39 | } 40 | 41 | 42 | return ( 43 | 50 | { children } 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/auth/context/authReducer.js: -------------------------------------------------------------------------------- 1 | import { types } from '../types/types'; 2 | 3 | export const authReducer = ( state = {}, action ) => { 4 | 5 | 6 | switch ( action.type ) { 7 | 8 | case types.login: 9 | return { 10 | ...state, 11 | logged: true, 12 | user: action.payload 13 | }; 14 | 15 | case types.logout: 16 | return { 17 | logged: false, 18 | }; 19 | 20 | default: 21 | return state; 22 | } 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/auth/context/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * from './AuthContext'; 4 | export * from './AuthProvider'; 5 | export * from './authReducer'; -------------------------------------------------------------------------------- /src/auth/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * from './context'; 4 | export * from './pages'; 5 | export * from './types/types' -------------------------------------------------------------------------------- /src/auth/pages/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { AuthContext } from '../context/AuthContext'; 4 | 5 | export const LoginPage = () => { 6 | 7 | const { login } = useContext( AuthContext ); 8 | const navigate = useNavigate(); 9 | 10 | const onLogin = () => { 11 | 12 | const lastPath = localStorage.getItem('lastPath') || '/'; 13 | 14 | login( 'Fernando Herrera' ); 15 | 16 | navigate( lastPath, { 17 | replace: true 18 | }); 19 | } 20 | 21 | return ( 22 |
23 |

Login

24 |
25 | 26 | 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/auth/pages/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * from './LoginPage'; -------------------------------------------------------------------------------- /src/auth/types/types.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const types = { 4 | login: '[Auth] Login', 5 | logout: '[Auth] Logout', 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/heroes/components/HeroCard.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | const CharactersByHero = ({ alter_ego, characters}) => { 4 | // if ( alter_ego === characters ) return (<>); 5 | // return

{ characters }

6 | return ( alter_ego === characters ) 7 | ? <> 8 | :

{ characters }

; 9 | } 10 | 11 | 12 | export const HeroCard = ({ 13 | id, 14 | superhero, 15 | publisher, 16 | alter_ego, 17 | first_appearance, 18 | characters , 19 | }) => { 20 | 21 | const heroImageUrl = `/assets/heroes/${ id }.jpg`; 22 | 23 | // const charactesByHero = (

{ characters }

); 24 | 25 | 26 | return ( 27 |
28 |
29 | 30 |
31 | 32 |
33 | { 34 |
35 | 36 |
37 | 38 |
39 | 40 |
{ superhero }
41 |

{ alter_ego }

42 | 43 | {/* { 44 | ( alter_ego !== characters ) && charactesByHero 45 | ( alter_ego !== characters ) &&

{ characters }

46 | } */} 47 | 48 | 49 |

50 | { first_appearance } 51 |

52 | 53 | 54 | Más.. 55 | 56 | 57 | 58 |
59 | 60 |
61 | 62 | 63 |
64 | 65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/heroes/components/HeroList.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useMemo } from 'react'; 3 | import { HeroCard } from './'; 4 | import { getHeroesByPublisher } from '../helpers'; 5 | 6 | export const HeroList = ({ publisher }) => { 7 | 8 | const heroes = useMemo( () => getHeroesByPublisher( publisher ), [ publisher ]); 9 | 10 | return ( 11 |
12 | { 13 | heroes.map( hero => ( 14 | 18 | )) 19 | } 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/heroes/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './HeroCard' 2 | export * from './HeroList' -------------------------------------------------------------------------------- /src/heroes/data/heroes.js: -------------------------------------------------------------------------------- 1 | export const heroes = [ 2 | { 3 | 'id': 'dc-batman', 4 | 'superhero':'Batman', 5 | 'publisher':'DC Comics', 6 | 'alter_ego':'Bruce Wayne', 7 | 'first_appearance':'Detective Comics #27', 8 | 'characters':'Bruce Wayne' 9 | }, 10 | { 11 | 'id': 'dc-superman', 12 | 'superhero':'Superman', 13 | 'publisher':'DC Comics', 14 | 'alter_ego':'Kal-El', 15 | 'first_appearance':'Action Comics #1', 16 | 'characters':'Kal-El' 17 | }, 18 | { 19 | 'id': 'dc-flash', 20 | 'superhero':'Flash', 21 | 'publisher':'DC Comics', 22 | 'alter_ego':'Jay Garrick', 23 | 'first_appearance':'Flash Comics #1', 24 | 'characters':'Jay Garrick, Barry Allen, Wally West, Bart Allen' 25 | }, 26 | { 27 | 'id': 'dc-green', 28 | 'superhero':'Green Lantern', 29 | 'publisher':'DC Comics', 30 | 'alter_ego':'Alan Scott', 31 | 'first_appearance':'All-American Comics #16', 32 | 'characters':'Alan Scott, Hal Jordan, Guy Gardner, John Stewart, Kyle Raynor, Jade, Sinestro, Simon Baz' 33 | }, 34 | { 35 | 'id': 'dc-arrow', 36 | 'superhero':'Green Arrow', 37 | 'publisher':'DC Comics', 38 | 'alter_ego':'Oliver Queen', 39 | 'first_appearance':'More Fun Comics #73', 40 | 'characters':'Oliver Queen' 41 | }, 42 | { 43 | 'id': 'dc-wonder', 44 | 'superhero':'Wonder Woman', 45 | 'publisher':'DC Comics', 46 | 'alter_ego':'Princess Diana', 47 | 'first_appearance':'All Star Comics #8', 48 | 'characters':'Princess Diana' 49 | }, 50 | { 51 | 'id': 'dc-martian', 52 | 'superhero':'Martian Manhunter', 53 | 'publisher':'DC Comics', 54 | 'alter_ego':'J\'onn J\'onzz', 55 | 'first_appearance':'Detective Comics #225', 56 | 'characters':'Martian Manhunter' 57 | }, 58 | { 59 | 'id': 'dc-robin', 60 | 'superhero':'Robin/Nightwing', 61 | 'publisher':'DC Comics', 62 | 'alter_ego':'Dick Grayson', 63 | 'first_appearance':'Detective Comics #38', 64 | 'characters':'Dick Grayson' 65 | }, 66 | { 67 | 'id': 'dc-blue', 68 | 'superhero':'Blue Beetle', 69 | 'publisher':'DC Comics', 70 | 'alter_ego':'Dan Garret', 71 | 'first_appearance':'Mystery Men Comics #1', 72 | 'characters':'Dan Garret, Ted Kord, Jaime Reyes' 73 | }, 74 | { 75 | 'id': 'dc-black', 76 | 'superhero':'Black Canary', 77 | 'publisher':'DC Comics', 78 | 'alter_ego':'Dinah Drake', 79 | 'first_appearance':'Flash Comics #86', 80 | 'characters':'Dinah Drake, Dinah Lance' 81 | }, 82 | { 83 | 'id': 'marvel-spider', 84 | 'superhero':'Spider Man', 85 | 'publisher':'Marvel Comics', 86 | 'alter_ego':'Peter Parker', 87 | 'first_appearance':'Amazing Fantasy #15', 88 | 'characters':'Peter Parker' 89 | }, 90 | { 91 | 'id': 'marvel-captain', 92 | 'superhero':'Captain America', 93 | 'publisher':'Marvel Comics', 94 | 'alter_ego':'Steve Rogers', 95 | 'first_appearance':'Captain America Comics #1', 96 | 'characters':'Steve Rogers' 97 | }, 98 | { 99 | 'id': 'marvel-iron', 100 | 'superhero':'Iron Man', 101 | 'publisher':'Marvel Comics', 102 | 'alter_ego':'Tony Stark', 103 | 'first_appearance':'Tales of Suspense #39', 104 | 'characters':'Tony Stark' 105 | }, 106 | { 107 | 'id': 'marvel-thor', 108 | 'superhero':'Thor', 109 | 'publisher':'Marvel Comics', 110 | 'alter_ego':'Thor Odinson', 111 | 'first_appearance':'Journey into Myster #83', 112 | 'characters':'Thor Odinson' 113 | }, 114 | { 115 | 'id': 'marvel-hulk', 116 | 'superhero':'Hulk', 117 | 'publisher':'Marvel Comics', 118 | 'alter_ego':'Bruce Banner', 119 | 'first_appearance':'The Incredible Hulk #1', 120 | 'characters':'Bruce Banner' 121 | }, 122 | { 123 | 'id': 'marvel-wolverine', 124 | 'superhero':'Wolverine', 125 | 'publisher':'Marvel Comics', 126 | 'alter_ego':'James Howlett', 127 | 'first_appearance':'The Incredible Hulk #180', 128 | 'characters':'James Howlett' 129 | }, 130 | { 131 | 'id': 'marvel-daredevil', 132 | 'superhero':'Daredevil', 133 | 'publisher':'Marvel Comics', 134 | 'alter_ego':'Matthew Michael Murdock', 135 | 'first_appearance':'Daredevil #1', 136 | 'characters':'Matthew Michael Murdock' 137 | }, 138 | { 139 | 'id': 'marvel-hawkeye', 140 | 'superhero':'Hawkeye', 141 | 'publisher':'Marvel Comics', 142 | 'alter_ego':'Clinton Francis Barton', 143 | 'first_appearance':'Tales of Suspense #57', 144 | 'characters':'Clinton Francis Barton' 145 | }, 146 | { 147 | 'id': 'marvel-cyclops', 148 | 'superhero':'Cyclops', 149 | 'publisher':'Marvel Comics', 150 | 'alter_ego':'Scott Summers', 151 | 'first_appearance':'X-Men #1', 152 | 'characters':'Scott Summers' 153 | }, 154 | { 155 | 'id': 'marvel-silver', 156 | 'superhero':'Silver Surfer', 157 | 'publisher':'Marvel Comics', 158 | 'alter_ego':'Norrin Radd', 159 | 'first_appearance':'The Fantastic Four #48', 160 | 'characters':'Norrin Radd' 161 | } 162 | ] -------------------------------------------------------------------------------- /src/heroes/helpers/getHeroById.js: -------------------------------------------------------------------------------- 1 | import { heroes } from '../data/heroes'; 2 | 3 | 4 | export const getHeroById = ( id ) => { 5 | 6 | return heroes.find( hero => hero.id === id ); 7 | } -------------------------------------------------------------------------------- /src/heroes/helpers/getHeroesByName.js: -------------------------------------------------------------------------------- 1 | import { heroes } from '../data/heroes'; 2 | 3 | 4 | export const getHeroesByName = ( name = '' ) => { 5 | 6 | name = name.toLocaleLowerCase().trim(); 7 | 8 | if ( name.length === 0 ) return []; 9 | 10 | return heroes.filter( 11 | hero => hero.superhero.toLocaleLowerCase().includes( name ) 12 | ); 13 | 14 | } 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/heroes/helpers/getHeroesByPublisher.js: -------------------------------------------------------------------------------- 1 | import { heroes } from '../data/heroes'; 2 | 3 | 4 | export const getHeroesByPublisher = ( publisher ) =>{ 5 | 6 | const validPublishers = ['DC Comics','Marvel Comics']; 7 | if ( !validPublishers.includes( publisher ) ) { 8 | throw new Error(`${ publisher } is not a valid publisher`); 9 | } 10 | 11 | return heroes.filter( heroe => heroe.publisher === publisher ); 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/heroes/helpers/index.js: -------------------------------------------------------------------------------- 1 | export * from './getHeroById'; 2 | export * from './getHeroesByName'; 3 | export * from './getHeroesByPublisher'; -------------------------------------------------------------------------------- /src/heroes/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * from './pages'; 4 | export * from './routes/HeroesRoutes'; 5 | -------------------------------------------------------------------------------- /src/heroes/pages/DcPage.jsx: -------------------------------------------------------------------------------- 1 | import { HeroList } from '../components'; 2 | 3 | 4 | export const DcPage = () => { 5 | return ( 6 | <> 7 |

DC Comics

8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/heroes/pages/HeroPage.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Navigate, useNavigate, useParams } from 'react-router-dom'; 3 | import { getHeroById } from '../helpers'; 4 | 5 | 6 | export const HeroPage = () => { 7 | 8 | const { id } = useParams(); 9 | const navigate = useNavigate(); 10 | 11 | const hero = useMemo( () => getHeroById( id ), [ id ]); 12 | 13 | const onNavigateBack = () => { 14 | navigate(-1); 15 | } 16 | 17 | 18 | if ( !hero ) { 19 | return 20 | } 21 | 22 | return ( 23 |
24 |
25 | { 30 |
31 | 32 |
33 |

{ hero.superhero }

34 |
    35 |
  • Alter ego: { hero.alter_ego }
  • 36 |
  • Publisher: { hero.publisher }
  • 37 |
  • First appearance: { hero.first_appearance }
  • 38 |
39 | 40 |
Characters
41 |

{ hero.characters }

42 | 43 | 49 | 50 |
51 | 52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/heroes/pages/MarvelPage.jsx: -------------------------------------------------------------------------------- 1 | import { HeroList } from '../components'; 2 | 3 | export const MarvelPage = () => { 4 | return ( 5 | <> 6 |

Marvel Comics

7 |
8 | 9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/heroes/pages/SearchPage.jsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useNavigate } from 'react-router-dom'; 2 | import queryString from 'query-string' 3 | 4 | import { useForm } from '../../hooks/useForm'; 5 | import { HeroCard } from '../components'; 6 | import { getHeroesByName } from '../helpers'; 7 | 8 | export const SearchPage = () => { 9 | 10 | const navigate = useNavigate(); 11 | const location = useLocation(); 12 | 13 | const { q = '' } = queryString.parse( location.search ); 14 | const heroes = getHeroesByName(q); 15 | 16 | const showSearch = (q.length === 0); 17 | const showError = (q.length > 0) && heroes.length === 0; 18 | 19 | 20 | const { searchText, onInputChange } = useForm({ 21 | searchText: q 22 | }); 23 | 24 | 25 | 26 | const onSearchSubmit = (event) =>{ 27 | event.preventDefault(); 28 | // if ( searchText.trim().length <= 1 ) return; 29 | navigate(`?q=${ searchText }`); 30 | } 31 | 32 | 33 | return ( 34 | <> 35 |

Search

36 |
37 | 38 |
39 | 40 |
41 |

Searching

42 |
43 |
44 | 53 | 54 | 57 |
58 |
59 | 60 |
61 |

Results

62 |
63 | 64 | {/* { 65 | ( q === '' ) 66 | ?
Search a hero
67 | : ( heroes.length === 0 ) 68 | &&
No hero with { q }
69 | } */} 70 | 71 |
73 | Search a hero 74 |
75 | 76 |
78 | No hero with { q } 79 |
80 | 81 | 82 | { 83 | heroes.map( hero => ( 84 | 85 | )) 86 | } 87 | 88 |
89 |
90 | 91 | 92 | 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /src/heroes/pages/index.js: -------------------------------------------------------------------------------- 1 | 2 | export * from './DcPage'; 3 | export * from './HeroPage'; 4 | export * from './MarvelPage'; 5 | export * from './SearchPage'; -------------------------------------------------------------------------------- /src/heroes/routes/HeroesRoutes.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Route, Routes } from 'react-router-dom'; 2 | import { Navbar } from '../../ui'; 3 | import { DcPage, HeroPage, MarvelPage, SearchPage } from '../pages'; 4 | 5 | export const HeroesRoutes = () => { 6 | return ( 7 | <> 8 | 9 | 10 |
11 | 12 | } /> 13 | } /> 14 | 15 | } /> 16 | } /> 17 | 18 | 19 | } /> 20 | 21 | 22 |
23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const useForm = ( initialForm = {} ) => { 4 | 5 | const [ formState, setFormState ] = useState( initialForm ); 6 | 7 | const onInputChange = ({ target }) => { 8 | const { name, value } = target; 9 | setFormState({ 10 | ...formState, 11 | [ name ]: value 12 | }); 13 | } 14 | 15 | const onResetForm = () => { 16 | setFormState( initialForm ); 17 | } 18 | 19 | return { 20 | ...formState, 21 | formState, 22 | onInputChange, 23 | onResetForm, 24 | } 25 | } -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | 5 | import { HeroesApp } from './HeroesApp'; 6 | import './styles.css'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root')).render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/router/AppRouter.jsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from 'react-router-dom'; 2 | 3 | import { HeroesRoutes } from '../heroes'; 4 | import { LoginPage } from '../auth'; 5 | import { PrivateRoute } from './PrivateRoute'; 6 | import { PublicRoute } from './PublicRoute'; 7 | 8 | 9 | 10 | export const AppRouter = () => { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 18 | {/* */} 19 | 20 | } /> 21 | 22 | 23 | } 24 | /> 25 | 26 | 27 | 29 | 30 | 31 | } /> 32 | 33 | {/* } /> */} 34 | {/* } /> */} 35 | 36 | 37 | 38 | 39 | 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/router/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { Navigate, useLocation } from 'react-router-dom'; 3 | 4 | import { AuthContext } from '../auth'; 5 | 6 | 7 | export const PrivateRoute = ({ children }) => { 8 | 9 | const { logged } = useContext( AuthContext ); 10 | const { pathname, search } = useLocation(); 11 | 12 | const lastPath = pathname + search; 13 | localStorage.setItem('lastPath', lastPath ); 14 | 15 | 16 | return (logged) 17 | ? children 18 | : 19 | } 20 | -------------------------------------------------------------------------------- /src/router/PublicRoute.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { Navigate } from 'react-router-dom'; 3 | 4 | import { AuthContext } from '../auth'; 5 | 6 | 7 | export const PublicRoute = ({ children }) => { 8 | 9 | const { logged } = useContext( AuthContext ); 10 | 11 | return (!logged) 12 | ? children 13 | : 14 | } 15 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/docker-react-heroes/e8d7862a0bbd3590cc9046151220918f684576eb/src/styles.css -------------------------------------------------------------------------------- /src/ui/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { Link, NavLink, useNavigate } from 'react-router-dom'; 3 | import { AuthContext } from '../../auth/context/AuthContext'; 4 | 5 | 6 | export const Navbar = () => { 7 | 8 | const { user, logout } = useContext( AuthContext ); 9 | 10 | 11 | const navigate = useNavigate(); 12 | 13 | const onLogout = () => { 14 | logout(); 15 | navigate('/login', { 16 | replace: true 17 | }); 18 | } 19 | 20 | return ( 21 | 73 | ) 74 | } -------------------------------------------------------------------------------- /src/ui/components/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * from './Navbar'; -------------------------------------------------------------------------------- /src/ui/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * from './components'; -------------------------------------------------------------------------------- /tests/auth/context/authReducer.test.js: -------------------------------------------------------------------------------- 1 | import { authReducer, types } from "../../../src/auth"; 2 | 3 | 4 | describe('Pruebas en authReducer', () => { 5 | 6 | test('debe de retornar el estado por defecto', () => { 7 | 8 | const state = authReducer({ logged: false }, {}); 9 | expect( state ).toEqual({ logged: false }); 10 | 11 | }); 12 | 13 | test('debe de (login) llamar el login autenticar y establecer el user', () => { 14 | 15 | const action = { 16 | type: types.login, 17 | payload: { 18 | name: 'Juan', 19 | id: '123' 20 | } 21 | } 22 | 23 | const state = authReducer({ logged: false }, action ); 24 | expect( state ).toEqual({ 25 | logged: true, 26 | user: action.payload 27 | }) 28 | 29 | }); 30 | 31 | test('debe de (logout) borrar el name del usuario y logged en false ', () => { 32 | 33 | const state = { 34 | logged: true, 35 | user: { id: '123', name: 'Juan' } 36 | } 37 | 38 | const action = { 39 | type: types.logout 40 | } 41 | 42 | const newState = authReducer( state, action ); 43 | expect( newState ).toEqual({ logged: false }) 44 | 45 | }); 46 | 47 | 48 | 49 | }); -------------------------------------------------------------------------------- /tests/auth/types/types.test.js: -------------------------------------------------------------------------------- 1 | import { types } from "../../../src/auth/types/types"; 2 | 3 | 4 | describe('Pruebas en "Types.js"', () => { 5 | 6 | test('debe de regresar estos types', () => { 7 | 8 | expect(types).toEqual({ 9 | login: '[Auth] Login', 10 | logout: '[Auth] Logout', 11 | }) 12 | 13 | }); 14 | 15 | }); -------------------------------------------------------------------------------- /tests/heroes/pages/SearchPage.test.jsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import { SearchPage } from '../../../src/heroes/pages/SearchPage'; 4 | 5 | const mockedUseNavigate = jest.fn(); 6 | 7 | jest.mock('react-router-dom', () => ({ 8 | ...jest.requireActual('react-router-dom'), 9 | useNavigate: () => mockedUseNavigate, 10 | })); 11 | 12 | 13 | 14 | describe('Pruebas en ', () => { 15 | 16 | 17 | beforeEach(() => jest.clearAllMocks() ); 18 | 19 | 20 | test('debe de mostrarse correactamente con valores por defecto', () => { 21 | 22 | const { container } =render( 23 | 24 | 25 | 26 | ); 27 | expect( container ).toMatchSnapshot(); 28 | 29 | }); 30 | 31 | test('debe de mostrar a Batman y el input con el valor del queryString', () => { 32 | 33 | render( 34 | 35 | 36 | 37 | ); 38 | 39 | const input = screen.getByRole('textbox'); 40 | expect( input.value ).toBe('batman'); 41 | 42 | const img = screen.getByRole('img'); 43 | expect( img.src ).toContain('/assets/heroes/dc-batman.jpg'); 44 | 45 | const alert = screen.getByLabelText('alert-danger'); 46 | expect( alert.style.display ).toBe('none'); 47 | 48 | }); 49 | 50 | test('debe de mostrar un error si no se encuentra el hero (batman123)', () => { 51 | 52 | render( 53 | 54 | 55 | 56 | ); 57 | 58 | const alert = screen.getByLabelText('alert-danger'); 59 | expect( alert.style.display ).toBe(''); 60 | 61 | 62 | }); 63 | 64 | test('debe de llamar el navigate a la pantalla nueva', () => { 65 | 66 | const inputValue = 'superman'; 67 | 68 | render( 69 | 70 | 71 | 72 | ); 73 | 74 | const input = screen.getByRole('textbox'); 75 | fireEvent.change( input, { target: { name: 'searchText', value: inputValue }}) 76 | 77 | 78 | const form = screen.getByRole('form'); 79 | fireEvent.submit( form ); 80 | 81 | expect( mockedUseNavigate ).toHaveBeenCalledWith(`?q=${ inputValue }`) 82 | 83 | }); 84 | 85 | 86 | }); -------------------------------------------------------------------------------- /tests/heroes/pages/__snapshots__/SearchPage.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Pruebas en debe de mostrarse correactamente con valores por defecto 1`] = ` 4 |
5 |

6 | Search 7 |

8 |
9 |
12 |
15 |

16 | Searching 17 |

18 |
19 |
22 | 30 | 35 |
36 |
37 |
40 |

41 | Results 42 |

43 |
44 |
47 | Search a hero 48 |
49 | 57 |
58 |
59 |
60 | `; 61 | -------------------------------------------------------------------------------- /tests/router/AppRouter.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import { AuthContext } from '../../src/auth'; 4 | import { AppRouter } from '../../src/router/AppRouter'; 5 | 6 | describe('Pruebas en ', () => { 7 | 8 | test('debe de mostrar el login si no está autenticado', () => { 9 | 10 | const contextValue = { 11 | logged: false, 12 | } 13 | 14 | render( 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | expect( screen.getAllByText('Login').length ).toBe(2) 23 | 24 | 25 | }); 26 | 27 | test('debe de mostrar el componente de Marvel si está autenticado', () => { 28 | 29 | const contextValue = { 30 | logged: true, 31 | user: { 32 | id: 'ABC', 33 | name: 'Juan Carlos' 34 | } 35 | } 36 | 37 | render( 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | 45 | expect( screen.getAllByText('Marvel').length ).toBeGreaterThanOrEqual(1); 46 | 47 | 48 | 49 | }); 50 | 51 | 52 | }); -------------------------------------------------------------------------------- /tests/router/PrivateRoute.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import { AuthContext } from '../../src/auth'; 4 | import { PrivateRoute } from '../../src/router/PrivateRoute'; 5 | 6 | 7 | describe('Pruebas en el ', () => { 8 | 9 | test('debe de mostrar el children si está autenticado', () => { 10 | 11 | Storage.prototype.setItem = jest.fn(); 12 | 13 | 14 | const contextValue = { 15 | logged: true, 16 | user: { 17 | id: 'abc', 18 | name: 'Juan Carlos' 19 | } 20 | } 21 | 22 | render( 23 | 24 | 25 | 26 |

Ruta privada

27 |
28 |
29 |
30 | ); 31 | 32 | expect( screen.getByText('Ruta privada') ).toBeTruthy(); 33 | expect( localStorage.setItem ).toHaveBeenCalledWith('lastPath', '/search?q=batman'); 34 | 35 | }); 36 | 37 | 38 | 39 | }); -------------------------------------------------------------------------------- /tests/router/PublicRoute.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { MemoryRouter, Route, Routes } from 'react-router-dom'; 3 | 4 | import { AuthContext } from '../../src/auth'; 5 | import { PublicRoute } from '../../src/router/PublicRoute'; 6 | 7 | 8 | describe('Pruebas en ', () => { 9 | 10 | test('debe de mostrar el children si no está autenticado', () => { 11 | 12 | const contextValue = { 13 | logged: false 14 | } 15 | 16 | render( 17 | 18 | 19 |

Ruta pública

20 |
21 |
22 | ); 23 | 24 | expect( screen.getByText('Ruta pública') ).toBeTruthy(); 25 | 26 | }); 27 | 28 | 29 | test('debe de navegar si está autenticado', () => { 30 | 31 | 32 | const contextValue = { 33 | logged: true, 34 | user: { 35 | name: 'Strider', 36 | id: 'ABC123' 37 | } 38 | } 39 | 40 | render( 41 | 42 | 43 | 44 | 45 | 47 |

Ruta pública

48 |
49 | } /> 50 | Página Marvel } /> 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | 58 | expect( screen.getByText('Página Marvel') ).toBeTruthy(); 59 | 60 | 61 | }) 62 | 63 | }); -------------------------------------------------------------------------------- /tests/ui/components/Navbar.test.jsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react'; 2 | import { MemoryRouter, useNavigate } from 'react-router-dom'; 3 | 4 | import { AuthContext } from '../../../src/auth/context/AuthContext'; 5 | import { Navbar } from '../../../src/ui/components/Navbar'; 6 | 7 | const mockedUseNavigate = jest.fn(); 8 | 9 | jest.mock('react-router-dom', () => ({ 10 | ...jest.requireActual('react-router-dom'), 11 | useNavigate: () => mockedUseNavigate, 12 | })); 13 | 14 | 15 | describe('Pruebas en ', () => { 16 | 17 | const contextValue = { 18 | logged: true, 19 | user: { 20 | name: 'Juan Carlos' 21 | }, 22 | logout: jest.fn() 23 | } 24 | 25 | beforeEach(() => jest.clearAllMocks() ); 26 | 27 | 28 | test('debe de mostrar el nombre del usuario', () => { 29 | 30 | render( 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | 38 | expect( screen.getByText('Juan Carlos') ).toBeTruthy(); 39 | 40 | 41 | }); 42 | 43 | test('debe de llamar el logout y navigate cuando se hace click en el botón', () => { 44 | 45 | render( 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | 53 | const logoutBtn = screen.getByRole('button'); 54 | fireEvent.click( logoutBtn ); 55 | 56 | expect( contextValue.logout ).toHaveBeenCalled() 57 | expect( mockedUseNavigate ).toHaveBeenCalledWith('/login', {"replace": true}) 58 | 59 | 60 | }); 61 | 62 | 63 | }); 64 | 65 | 66 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | --------------------------------------------------------------------------------