├── .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 |
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 |
36 |
37 |
40 |
41 | Results
42 |
43 |
44 |
47 | Search a hero
48 |
49 |
54 | No hero with
55 |
56 |
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 |
--------------------------------------------------------------------------------