diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index d8b83df..0000000
--- a/.eslintignore
+++ /dev/null
@@ -1 +0,0 @@
-package-lock.json
diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index 3480d73..0000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,63 +0,0 @@
-module.exports = {
- env: {
- browser: true,
- es2021: true,
- node: true,
- jest: true,
- jquery: true,
- "jest/globals": true,
- },
- extends: [
- "airbnb",
- "eslint:recommended",
- "plugin:compat/recommended",
- "plugin:react/recommended",
- "plugin:jest/recommended",
- "prettier",
- "prettier/prettier",
- "plugin:import/errors",
- "plugin:import/warnings",
- "plugin:jsx-a11y/recommended",
- "react-app",
- "react-app/jest",
- ],
- parser: "@babel/eslint-parser",
- parserOptions: {
- ecmaVersion: 12,
- sourceType: "module",
- requireConfigFile: "false",
- jsx: true,
- },
- plugins: [
- "@html-eslint",
- "jest",
- "react",
- "jsx-a11y",
- "markdown",
- "react-hooks",
- "import",
- ],
- settings: {
- react: {
- version: "detect",
- },
- },
- overrides: [
- {
- files: ["*.html"],
- parser: "@html-eslint/parser",
- extends: ["plugin:@html-eslint/recommended"],
- },
- ],
- rules: {
- "react/jsx-filename-extension": "off",
- "import/prefer-default-export": "off",
- "prefer-destructuring": "off",
- "object-shorthand": "off",
- "react/jsx-props-no-spreading": "off",
- "arrow-body-style": "off",
- "no-underscore-dangle": "off",
- "react/forbid-prop-types": "off",
- "react/prop-types": "off",
- },
-};
diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index 8c4b2fa..0000000
--- a/.prettierrc
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "arrowParens": "always",
- "bracketSpacing": true,
- "embeddedLanguageFormatting": "auto",
- "htmlWhitespaceSensitivity": "css",
- "insertPragma": false,
- "jsxBracketSameLine": false,
- "jsxSingleQuote": false,
- "printWidth": 80,
- "proseWrap": "always",
- "quoteProps": "as-needed",
- "requirePragma": false,
- "semi": true,
- "singleQuote": false,
- "tabWidth": 2,
- "trailingComma": "all",
- "useTabs": false,
- "endOfLine": "lf"
-}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..f87386d
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,15 @@
+{
+ // Use IntelliSense para saber los atributos posibles.
+ // Mantenga el puntero para ver las descripciones de los existentes atributos.
+ // Para más información, visite: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "pwa-chrome",
+ "request": "launch",
+ "name": "Launch Chrome against localhost",
+ "url": "http://localhost:8080",
+ "webRoot": "${workspaceFolder}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index eedc843..47682bc 100644
--- a/README.md
+++ b/README.md
@@ -82,37 +82,31 @@ the requirements document.**
Path: `/`
-
-
-### Home page disabled pagination
-
-Path: `/`
-
-
+
### Episode page
Path: `/episode/:episodeId`
-
+
### Character page
Path: `/character/:characterId`
-
+
### Location page
Path: `/location/:locationId`
-
+
-## Project delivery
+## Postman Docs
-To deliver this project you must follow the steps indicated in the document:
+The documentation for the Rick & Morty request collection can be found in the next resource:
-- [Submitting a solution](https://www.notion.so/Submitting-a-solution-524dab1a71dd4b96903f26385e24cdb6)
+https://documenter.getpostman.com/view/16968133/UUy68RF5
## Resources
diff --git a/package-lock.json b/package-lock.json
index 993b456..2dbbb5b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1245,6 +1245,29 @@
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
},
+ "@emotion/is-prop-valid": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
+ "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
+ "requires": {
+ "@emotion/memoize": "0.7.4"
+ }
+ },
+ "@emotion/memoize": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
+ "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
+ },
+ "@emotion/stylis": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
+ "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
+ },
+ "@emotion/unitless": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
+ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
+ },
"@eslint/eslintrc": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz",
@@ -3022,6 +3045,22 @@
"@babel/helper-define-polyfill-provider": "^0.1.5"
}
},
+ "babel-plugin-styled-components": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz",
+ "integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==",
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.0.0",
+ "@babel/helper-module-imports": "^7.0.0",
+ "babel-plugin-syntax-jsx": "^6.18.0",
+ "lodash": "^4.17.11"
+ }
+ },
+ "babel-plugin-syntax-jsx": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
+ "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
+ },
"babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
@@ -3742,6 +3781,11 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
"integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg=="
},
+ "camelize": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
+ "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
+ },
"caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -4434,6 +4478,11 @@
"postcss": "^7.0.5"
}
},
+ "css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
+ },
"css-color-names": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
@@ -4517,6 +4566,16 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
+ "css-to-react-native": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
+ "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
+ "requires": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -13347,6 +13406,11 @@
"safe-buffer": "^5.0.1"
}
},
+ "shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -14014,6 +14078,38 @@
"schema-utils": "^2.7.0"
}
},
+ "styled-components": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.1.tgz",
+ "integrity": "sha512-JThv2JRzyH0NOIURrk9iskdxMSAAtCfj/b2Sf1WJaCUsloQkblepy1jaCLX/bYE+mhYo3unmwVSI9I5d9ncSiQ==",
+ "requires": {
+ "@babel/helper-module-imports": "^7.0.0",
+ "@babel/traverse": "^7.4.5",
+ "@emotion/is-prop-valid": "^0.8.8",
+ "@emotion/stylis": "^0.8.4",
+ "@emotion/unitless": "^0.7.4",
+ "babel-plugin-styled-components": ">= 1.12.0",
+ "css-to-react-native": "^3.0.0",
+ "hoist-non-react-statics": "^3.0.0",
+ "shallowequal": "^1.1.0",
+ "supports-color": "^5.5.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
"stylehacks": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
diff --git a/package.json b/package.json
index a154f9e..87ca463 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"sass": "^1.32.11",
+ "styled-components": "^5.3.1",
"web-vitals": "^1.0.1"
},
"devDependencies": {
diff --git a/public/index.html b/public/index.html
index aa069f2..7e0a134 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,43 +1,21 @@
-
-
-
-
-
-
-
-
-
-
- React App
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
diff --git a/rick_and_morty.postman_collection.json b/rick_and_morty.postman_collection.json
new file mode 100644
index 0000000..f461355
--- /dev/null
+++ b/rick_and_morty.postman_collection.json
@@ -0,0 +1,203 @@
+{
+ "info": {
+ "_postman_id": "06b9bbf6-2238-43c9-9c73-dc296ff2b875",
+ "name": "Rick & Morty",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "Episode",
+ "item": [
+ {
+ "name": "Get Episodes",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{base_url}}/episode?page={{page}}",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "episode"
+ ],
+ "query": [
+ {
+ "key": "page",
+ "value": "{{page}}"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get Single Episode",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{base_url}}/episode/{{episode_id}}",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "episode",
+ "{{episode_id}}"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Character",
+ "item": [
+ {
+ "name": "Get Characters",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{base_url}}/character?page={{page}}",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "character"
+ ],
+ "query": [
+ {
+ "key": "page",
+ "value": "{{page}}"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get Single Character",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{base_url}}/character/{{character_id}}",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "character",
+ "{{character_id}}"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Location",
+ "item": [
+ {
+ "name": "Get Locations",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{base_url}}/location?page={{page}}",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "location"
+ ],
+ "query": [
+ {
+ "key": "page",
+ "value": "{{page}}"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get Single Location",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{base_url}}/location/{{location_id}}",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "location",
+ "{{location_id}}"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Resources",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{base_url}}",
+ "host": [
+ "{{base_url}}"
+ ]
+ }
+ },
+ "response": []
+ }
+ ],
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ }
+ ],
+ "variable": [
+ {
+ "key": "episode_id",
+ "value": "1"
+ },
+ {
+ "key": "character_id",
+ "value": "1"
+ },
+ {
+ "key": "location_id",
+ "value": "1"
+ },
+ {
+ "key": "page",
+ "value": "1"
+ },
+ {
+ "key": "base_url",
+ "value": "https://rickandmortyapi.com/api"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/App.js b/src/App.js
index 3a49c0e..45ec85e 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,9 +1,60 @@
import React from "react";
-
+import { Redirect, Route, Switch } from "react-router";
import Home from "./pages/Home";
+import Episode from "./pages/Episode";
+import Character from "./pages/Character";
+import Location from "./pages/Location";
+import { FontStyle, GlobalStyle } from "./theme";
+import { ThemeProvider } from "styled-components";
+import theme from "./theme";
function App() {
- return ;
+ return (
+
+
+
+
+
+
+
+ {
+ const page = renderProps.match.params.page;
+ return ;
+ }}
+ />
+ {
+ const id = renderProps.match.params.id;
+
+ return ;
+ }}
+ />
+ {
+ const id = renderProps.match.params.id;
+
+ return ;
+ }}
+ />
+ {
+ const id = renderProps.match.params.id;
+
+ return ;
+ }}
+ />
+
+
+ );
}
export default App;
diff --git a/src/api/requests.js b/src/api/requests.js
new file mode 100644
index 0000000..96feb04
--- /dev/null
+++ b/src/api/requests.js
@@ -0,0 +1,48 @@
+import * as routes from "../constants/routes.js";
+const path = require("path");
+
+export async function fetchData(url) {
+ const request = await fetch(url);
+
+ if (!request.ok) {
+ const error = {
+ url,
+ code: request.status,
+ message: request.statusText,
+ };
+
+ return { data: null, error };
+ }
+
+ const data = await request.json();
+
+ return { data, error: null };
+}
+
+export async function fetchDataset(urls) {
+ const results = await Promise.all(urls.map((url) => fetchData(url)));
+ const dataset = results.map((result) => result.data);
+ const errors = results.map((result) => result.error);
+
+ return { dataset, errors };
+}
+
+export async function getEpisodes({ page = 1 }) {
+ const url = `${path.join(routes.BASE_URL, routes.EPISODE)}?page=${page}`;
+ return fetchData(url);
+}
+
+export async function getEpisode({ id }) {
+ const url = path.join(routes.BASE_URL, routes.EPISODE, id);
+ return fetchData(url);
+}
+
+export async function getCharacter({ id }) {
+ const url = path.join(routes.BASE_URL, routes.CHARACTER, id);
+ return fetchData(url);
+}
+
+export async function getLocation({ id }) {
+ const url = path.join(routes.BASE_URL, routes.LOCATION, id);
+ return fetchData(url);
+}
diff --git a/src/api/requests.old.js b/src/api/requests.old.js
new file mode 100644
index 0000000..58759fa
--- /dev/null
+++ b/src/api/requests.old.js
@@ -0,0 +1,61 @@
+import * as routes from "../constants/routes.js";
+
+const path = require("path");
+
+export async function getEpisodes({ page = 1 }) {
+ const url = `${path.join(routes.BASE_URL, routes.EPISODE)}?page=${page}`;
+ const request = await fetch(url);
+ const data = await request.json();
+
+ if (!request.ok) throw Error(request.statusText);
+
+ return data;
+}
+
+export async function getEpisode({ episode }) {
+ const url = path.join(routes.BASE_URL, routes.EPISODE, episode);
+ const episodeRequest = await fetch(url);
+ const episodeData = await episodeRequest.json();
+
+ if (!episodeRequest.ok) throw Error(episodeRequest.statusText);
+
+ const charactersRequest = await Promise.all(episodeData.characters.map((characterURL) => fetch(characterURL)));
+ const charactersData = await Promise.all(charactersRequest.map((characterRequest) => characterRequest.json()));
+
+ return {
+ episode: episodeData,
+ characters: charactersData,
+ };
+}
+
+export async function getCharacter({ character }) {
+ const url = path.join(routes.BASE_URL, routes.CHARACTER, character);
+ const characterRequest = await fetch(url);
+ const characterData = await characterRequest.json();
+
+ if (!characterRequest.ok) throw Error(characterRequest.statusText);
+
+ const episodesRequest = await Promise.all(characterData.episode.map((episodeURL) => fetch(episodeURL)));
+ const episodesData = await Promise.all(episodesRequest.map((episodeRequest) => episodeRequest.json()));
+
+ return {
+ character: characterData,
+ episodes: episodesData,
+ };
+}
+
+export async function getLocation({ location }) {
+ const url = path.join(routes.BASE_URL, routes.LOCATION, location);
+ const locationRequest = await fetch(url);
+ const locationData = await locationRequest.json();
+
+ if (!locationRequest.ok) throw Error(locationRequest.statusText);
+
+ const charactersRequest = await Promise.all(locationData.residents.map((characterURL) => fetch(characterURL)));
+ const charactersData = await Promise.all(charactersRequest.map((characterRequest) => characterRequest.json()));
+
+ return {
+ location: locationData,
+ characters: charactersData,
+ };
+}
diff --git a/src/components/AppHeader/AppHeader.js b/src/components/AppHeader/AppHeader.js
deleted file mode 100644
index dfec840..0000000
--- a/src/components/AppHeader/AppHeader.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from "react";
-import { NavLink } from "react-router-dom";
-
-import * as routes from "../../constants/routes";
-
-function AppHeader({ ...props }) {
- return (
-
- );
-}
-
-export default AppHeader;
diff --git a/src/components/AppHeader/index.js b/src/components/AppHeader/index.js
deleted file mode 100644
index 8ad0d17..0000000
--- a/src/components/AppHeader/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./AppHeader";
diff --git a/src/components/Button/Button.js b/src/components/Button/Button.js
new file mode 100644
index 0000000..fdeaa5f
--- /dev/null
+++ b/src/components/Button/Button.js
@@ -0,0 +1,45 @@
+import { Link } from "react-router-dom";
+import styled, { css } from "styled-components";
+
+const ButtonStyle = css`
+ outline: none;
+ border: none;
+
+ display: block;
+ padding: 0.25rem 0.5rem;
+ min-width: 6rem;
+
+ font-size: 1.25rem;
+ text-align: center;
+
+ border-radius: 4px;
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
+ background: ${({ theme, $light }) => ($light ? theme.palette.dark.contrast : theme.palette.dark.main)};
+ color: ${({ theme, $light }) => ($light ? theme.palette.dark.main : theme.palette.dark.contrast)};
+
+ transition: all 0.125s ease-in-out;
+
+ &:hover {
+ text-decoration: none;
+
+ background: ${({ theme, $light }) => ($light ? theme.palette.dark.main : theme.palette.dark.contrast)};
+ color: ${({ theme, $light }) => ($light ? theme.palette.dark.contrast : theme.palette.dark.main)};
+ }
+
+ ${(props) =>
+ props.disabled &&
+ css`
+ pointer-events: none;
+ opacity: 0.75;
+ `}
+`;
+
+const ButtonBasic = styled.button`
+ ${ButtonStyle}
+`;
+
+const ButtonLink = styled(Link)`
+ ${ButtonStyle}
+`;
+
+export { ButtonBasic, ButtonLink };
diff --git a/src/components/Button/index.js b/src/components/Button/index.js
new file mode 100644
index 0000000..a46002f
--- /dev/null
+++ b/src/components/Button/index.js
@@ -0,0 +1 @@
+export { ButtonBasic, ButtonLink } from "./Button";
diff --git a/src/components/CharacterCard/CharacterCard.js b/src/components/CharacterCard/CharacterCard.js
index 9e7e4ab..1027397 100644
--- a/src/components/CharacterCard/CharacterCard.js
+++ b/src/components/CharacterCard/CharacterCard.js
@@ -1,29 +1,94 @@
-import React from "react";
import { Link } from "react-router-dom";
+import * as routes from "../../constants/routes";
-import "./CharacterCard.scss";
+import Flex from "../Flex";
-import * as routes from "../../constants/routes";
+import aliveImg from "../../images/heartbeat.png";
+import deadImg from "../../images/dead.png";
+
+import styled from "styled-components";
+
+const Article = styled.article`
+ border: 1px solid ${({ theme }) => theme.palette.dark.main};
+ border-radius: 4px;
+ background-color: ${({ theme }) => theme.palette.dark.secondary};
+`;
+
+const Image = styled.img`
+ display: block;
+ width: 15rem;
+ object-fit: cover;
+ border-radius: 4px 4px 0 0;
+`;
+
+const Icon = styled.img`
+ width: 1.5rem;
+ height: 1.5rem;
+ vertical-align: text-bottom;
+`;
+
+const Name = styled.h3`
+ padding: 0.75rem;
+ margin: 0;
+
+ font-size: 1.25rem;
+ text-align: center;
+ text-transform: uppercase;
+
+ background-color: ${({ theme }) => theme.palette.dark.main};
+`;
+
+const PropertyField = styled.span`
+ display: block;
+
+ text-align: center;
+ font-family: "Gemunu Libre";
+ font-weight: 700;
+`;
+
+const DataField = styled.span`
+ display: block;
+
+ text-align: center;
+ font-family: "Gemunu Libre";
+ font-weight: 300;
+`;
function CharacterCard({ id, name, image, species, status, origin, location }) {
- return (
-
-

-
-
{name}
-
-
-
- {origin.name}
-
-
|
-
{status}
-
-
- );
+ return (
+
+
+
+
+ {name}
+
+
+
+ Character type
+ {species}
+
+
+ Status
+
+
+
+
+
+
+ Last known location
+
+ {location.name} {location.url && 🔗}
+
+
+
+ Origin location
+
+ {origin.name} {origin.url && 🔗}
+
+
+
+
+ );
}
export default CharacterCard;
diff --git a/src/components/CharacterCard/CharacterCard.scss b/src/components/CharacterCard/CharacterCard.scss
deleted file mode 100644
index 7815354..0000000
--- a/src/components/CharacterCard/CharacterCard.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-.CharacterCard {
- margin-bottom: 1.5rem;
-
- &__img {
- display: block;
- width: 100%;
- border-radius: 3px;
- height: auto;
- max-height: 16rem;
- object-fit: cover;
- }
-
- &__name {
- margin: 0.75rem 0;
- }
-
- &__meta {
- display: flex;
- &-item {
- margin-right: 0.5rem;
- }
- }
-}
diff --git a/src/components/CharacterGrid/CharacterGrid.js b/src/components/CharacterGrid/CharacterGrid.js
new file mode 100644
index 0000000..edd5455
--- /dev/null
+++ b/src/components/CharacterGrid/CharacterGrid.js
@@ -0,0 +1,26 @@
+import styled from "styled-components";
+
+const CharacterGrid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(1, 1fr);
+ grid-template-rows: auto;
+ justify-items: center;
+ align-items: start;
+
+ padding: 1rem;
+ gap: 1rem 2rem;
+
+ @media screen and (min-width: ${({ theme }) => theme.breakpoints.sm}) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media screen and (min-width: ${({ theme }) => theme.breakpoints.md}) {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ @media screen and (min-width: ${({ theme }) => theme.breakpoints.lg}) {
+ grid-template-columns: repeat(4, 1fr);
+ }
+`;
+
+export default CharacterGrid;
diff --git a/src/components/CharacterGrid/index.js b/src/components/CharacterGrid/index.js
new file mode 100644
index 0000000..295227c
--- /dev/null
+++ b/src/components/CharacterGrid/index.js
@@ -0,0 +1 @@
+export { default } from "./CharacterGrid";
diff --git a/src/components/CharacterProfile/CharacterProfile.js b/src/components/CharacterProfile/CharacterProfile.js
new file mode 100644
index 0000000..32bfca1
--- /dev/null
+++ b/src/components/CharacterProfile/CharacterProfile.js
@@ -0,0 +1,102 @@
+import { Link } from "react-router-dom";
+import * as routes from "../../constants/routes";
+
+import styled from "styled-components";
+import Flex from "../Flex";
+
+const Article = styled.article`
+ margin: 0 auto;
+ width: min-content;
+
+ display: flex;
+ flex-direction: column;
+
+ border: 1px solid ${({ theme }) => theme.palette.dark.main};
+ border-radius: 4px;
+ background-color: ${({ theme }) => theme.palette.dark.secondary};
+
+ @media screen and (min-width: ${({ theme }) => theme.breakpoints.md}) {
+ flex-direction: row;
+ }
+`;
+
+const Image = styled.img`
+ display: block;
+ width: 15rem;
+ object-fit: contain;
+`;
+
+const InfoGrid = styled.div`
+ width: 15rem;
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: auto;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+
+ @media screen and (min-width: ${({ theme }) => theme.breakpoints.md}) {
+ width: 30rem;
+ grid-template-columns: repeat(2, 1fr);
+ }
+`;
+
+const Name = styled.h3`
+ padding: 0.75rem;
+ margin: 0;
+
+ font-size: 1.25rem;
+ text-align: center;
+ text-transform: uppercase;
+
+ background-color: ${({ theme }) => theme.palette.dark.main};
+`;
+
+const PropertyField = styled.span`
+ display: block;
+
+ font-size: 1.5rem;
+ font-family: "Gemunu Libre";
+ font-weight: 700;
+`;
+
+const DataField = styled.span`
+ display: block;
+
+ font-family: "Gemunu Libre";
+ font-weight: 300;
+`;
+
+export default function CharacterProfile(props) {
+ const { image, name, species, status, origin, location } = props;
+
+ return (
+
+
+
+ {name}
+
+
+ Character type
+ {species}
+
+
+ Status
+ {status}
+
+
+ Last known location
+
+ {location.name} {location.url && 🔗}
+
+
+
+ Origin location
+
+ {origin.name} {origin.url && 🔗}
+
+
+
+
+
+ );
+}
diff --git a/src/components/CharacterProfile/index.js b/src/components/CharacterProfile/index.js
new file mode 100644
index 0000000..7d56368
--- /dev/null
+++ b/src/components/CharacterProfile/index.js
@@ -0,0 +1 @@
+export { default } from "./CharacterProfile";
diff --git a/src/components/Container/Container.js b/src/components/Container/Container.js
new file mode 100644
index 0000000..6eb6a09
--- /dev/null
+++ b/src/components/Container/Container.js
@@ -0,0 +1,50 @@
+import styled, { css } from "styled-components";
+
+const Container = styled.div`
+ width: 100%;
+ margin: 0 auto;
+
+ ${(props) =>
+ props.xs &&
+ css`
+ @media (min-width: ${props.theme.breakpoints.xs}) {
+ width: ${props.theme.breakpoints.xs};
+ }
+ `}
+
+ ${(props) =>
+ props.sm &&
+ css`
+ @media (min-width: ${props.theme.breakpoints.sm}) {
+ width: ${props.theme.breakpoints.sm};
+ }
+ `}
+
+ ${(props) =>
+ props.md &&
+ css`
+ @media (min-width: ${props.theme.breakpoints.md}) {
+ width: ${props.theme.breakpoints.md};
+ }
+ `}
+
+ ${(props) =>
+ props.lg &&
+ css`
+ @media (min-width: ${props.theme.breakpoints.lg}) {
+ width: ${props.theme.breakpoints.lg};
+ }
+ `}
+
+ ${(props) =>
+ props.xl &&
+ css`
+ @media (min-width: ${props.theme.breakpoints.xl}) {
+ width: ${props.theme.breakpoints.xl};
+ }
+ `}
+
+ ${(props) => props.styles}
+`;
+
+export default Container;
diff --git a/src/components/Container/index.js b/src/components/Container/index.js
new file mode 100644
index 0000000..c81c92a
--- /dev/null
+++ b/src/components/Container/index.js
@@ -0,0 +1 @@
+export { default } from "./Container";
diff --git a/src/components/Divider/Divider.js b/src/components/Divider/Divider.js
new file mode 100644
index 0000000..d4fad7d
--- /dev/null
+++ b/src/components/Divider/Divider.js
@@ -0,0 +1,10 @@
+import styled from "styled-components";
+
+const Divider = styled.hr`
+ height: ${({ thickness }) => thickness || "2px"};
+
+ border-radius: 2px;
+ background-color: ${({ theme }) => theme.palette.dark.contrast};
+`;
+
+export default Divider;
diff --git a/src/components/Divider/index.js b/src/components/Divider/index.js
new file mode 100644
index 0000000..9802041
--- /dev/null
+++ b/src/components/Divider/index.js
@@ -0,0 +1 @@
+export { default } from "./Divider";
diff --git a/src/components/EpisodeCard/EpisodeCard.js b/src/components/EpisodeCard/EpisodeCard.js
index 13cb5cf..980fd8e 100644
--- a/src/components/EpisodeCard/EpisodeCard.js
+++ b/src/components/EpisodeCard/EpisodeCard.js
@@ -1,23 +1,42 @@
import React from "react";
import { Link } from "react-router-dom";
-import "./EpisodeCard.scss";
-
import * as routes from "../../constants/routes";
+import Flex from "../Flex";
+
+import styled from "styled-components";
+
+const Article = styled.article`
+ border: 1px solid ${({ theme }) => theme.palette.dark.main};
+ border-radius: 4px;
+ background-color: rgba(64, 64, 64, 0.5);
+`;
+
+const Name = styled.h3`
+ padding: 0.4rem;
+ margin: 0;
+
+ font-size: 1.25rem;
+ background-color: ${({ theme }) => theme.palette.dark.main};
+`;
+
+const Info = styled.p`
+ padding: 0.4rem;
+ margin: 0;
+`;
function EpisodeCard({ id, name, airDate, episode }) {
- return (
-
-
-
{name}
-
-
-
{airDate}
-
|
-
{episode}
-
-
- );
+ return (
+
+
+ {name}
+
+
+ {airDate}
+ {episode}
+
+
+ );
}
export default EpisodeCard;
diff --git a/src/components/EpisodeCard/EpisodeCard.scss b/src/components/EpisodeCard/EpisodeCard.scss
deleted file mode 100644
index 0924700..0000000
--- a/src/components/EpisodeCard/EpisodeCard.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-.Episode {
- &__img {
- display: block;
- width: 100%;
- border-radius: 3px;
- height: auto;
- max-height: 16rem;
- object-fit: cover;
- }
-
- &__name {
- margin: 0.75rem 0;
- }
-
- &__meta {
- display: flex;
- &-item {
- margin-right: 0.5rem;
- }
- }
-}
diff --git a/src/components/EpisodeGrid/EpisodeGrid.js b/src/components/EpisodeGrid/EpisodeGrid.js
new file mode 100644
index 0000000..fbec843
--- /dev/null
+++ b/src/components/EpisodeGrid/EpisodeGrid.js
@@ -0,0 +1,22 @@
+import styled from "styled-components";
+
+const EpisodeGrid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(1, 1fr);
+ grid-template-rows: auto;
+ justify-items: stretch;
+ align-items: start;
+
+ padding: 1rem;
+ gap: 1rem 2rem;
+
+ @media screen and (min-width: ${({ theme }) => theme.breakpoints.sm}) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media screen and (min-width: ${({ theme }) => theme.breakpoints.xl}) {
+ grid-template-columns: repeat(4, 1fr);
+ }
+`;
+
+export default EpisodeGrid;
diff --git a/src/components/EpisodeGrid/index.js b/src/components/EpisodeGrid/index.js
new file mode 100644
index 0000000..c7886b2
--- /dev/null
+++ b/src/components/EpisodeGrid/index.js
@@ -0,0 +1 @@
+export { default } from "./EpisodeGrid";
diff --git a/src/components/Flex/Flex.js b/src/components/Flex/Flex.js
new file mode 100644
index 0000000..b483701
--- /dev/null
+++ b/src/components/Flex/Flex.js
@@ -0,0 +1,15 @@
+import styled from "styled-components";
+
+const Flex = styled.div`
+ display: flex;
+ flex-wrap: ${(props) => props.wrap || "no-wrap"};
+ flex-direction: ${(props) => props.direction || "row"};
+ align-items: ${(props) => props.alignItems || "center"};
+ align-content: ${(props) => props.alignContent || "center"};
+ justify-content: ${(props) => props.justifyContent || "center"};
+ gap: ${(props) => props.gap || "0"};
+ margin: ${(props) => props.m || "0"};
+ padding: ${(props) => props.p || "0"};
+`;
+
+export default Flex;
diff --git a/src/components/Flex/index.js b/src/components/Flex/index.js
new file mode 100644
index 0000000..fd26cb6
--- /dev/null
+++ b/src/components/Flex/index.js
@@ -0,0 +1 @@
+export { default } from "./Flex";
diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js
index 8ed7b26..c28d99a 100644
--- a/src/components/Footer/Footer.js
+++ b/src/components/Footer/Footer.js
@@ -1,17 +1,28 @@
import React from "react";
+import styled from "styled-components";
+
+const StyledFooter = styled.footer`
+ height: 4rem;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ background: ${({ theme }) => theme.palette.dark.main};
+ color: ${({ theme }) => theme.palette.dark.contrast};
+`;
+
+const Copy = styled.p`
+ margin: 0;
+ text-align: center;
+`;
function Footer() {
- return (
-
- );
+ return (
+
+ Pill done by Sanadriu, with ♡
+
+ );
}
export default Footer;
diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js
new file mode 100644
index 0000000..ded6d64
--- /dev/null
+++ b/src/components/Header/Header.js
@@ -0,0 +1,45 @@
+import React from "react";
+
+import { ButtonLink } from "../Button";
+import styled from "styled-components";
+
+import * as routes from "../../constants/routes";
+import Container from "../Container";
+
+const HeaderStyled = styled.header`
+ height: 4rem;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ background: ${({ theme }) => theme.palette.dark.main};
+ color: ${({ theme }) => theme.palette.dark.contrast};
+`;
+
+const List = styled.ul`
+ padding: 0;
+ margin: 0;
+
+ display: flex;
+ flex-direction: row;
+ gap: 1rem;
+
+ list-style: none;
+`;
+
+export default function Header({ ...props }) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/Header/index.js b/src/components/Header/index.js
new file mode 100644
index 0000000..2764567
--- /dev/null
+++ b/src/components/Header/index.js
@@ -0,0 +1 @@
+export { default } from "./Header";
diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js
deleted file mode 100644
index a8bdcd3..0000000
--- a/src/components/Layout/Layout.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from "react";
-
-import AppHeader from "../AppHeader";
-import Main from "../Main";
-import Footer from "../Footer";
-
-function Layout({ children }) {
- return (
- <>
-
- {children}
-
- >
- );
-}
-
-export default Layout;
diff --git a/src/components/Layout/index.js b/src/components/Layout/index.js
deleted file mode 100644
index d4dca0d..0000000
--- a/src/components/Layout/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./Layout";
diff --git a/src/components/Main/Main.js b/src/components/Main/Main.js
index 43bb386..053a1d3 100644
--- a/src/components/Main/Main.js
+++ b/src/components/Main/Main.js
@@ -1,11 +1,30 @@
import React from "react";
+import styled, { css } from "styled-components";
+import wallpaper from "../../images/wallpaper.jpg";
+import Container from "../Container";
-function Main({ children, ...props }) {
- return (
-
- {children}
-
- );
-}
+const MainStyled = styled.main`
+ min-height: calc(100vh - 8rem);
+ background-color: purple;
+ background-image: url(${wallpaper});
+ background-size: cover;
+ background-position: center;
+ background-blend-mode: multiply;
+ background-attachment: fixed;
+
+ color: ${({ theme }) => theme.palette.dark.contrast};
+`;
-export default Main;
+const ContainerStyles = css`
+ padding: 1rem;
+`;
+
+export default function Main({ children, ...props }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/components/MessageCard/MessageCard.js b/src/components/MessageCard/MessageCard.js
new file mode 100644
index 0000000..7826c82
--- /dev/null
+++ b/src/components/MessageCard/MessageCard.js
@@ -0,0 +1,25 @@
+import errorImage from "../../images/error-image.png";
+import noResidentsImage from "../../images/rick-in-peace.jpg";
+
+export default function MessageCard({ image, message }) {
+ return (
+
+
+ {image &&

}
+
+
+
+ );
+}
+
+export function ErrorMessageCard(props) {
+ const { message = "OOPS! It seems that something went wrong." } = props;
+
+ return ;
+}
+
+export function NoResidentsCard() {
+ return ;
+}
diff --git a/src/components/MessageCard/index.js b/src/components/MessageCard/index.js
new file mode 100644
index 0000000..989c850
--- /dev/null
+++ b/src/components/MessageCard/index.js
@@ -0,0 +1 @@
+export { default, ErrorMessageCard, NoResidentsCard } from "./MessageCard";
diff --git a/src/components/SpinnerLoader/SpinnerLoader.js b/src/components/SpinnerLoader/SpinnerLoader.js
new file mode 100644
index 0000000..666a85a
--- /dev/null
+++ b/src/components/SpinnerLoader/SpinnerLoader.js
@@ -0,0 +1,12 @@
+import "./SpinnerLoader.scss";
+
+export default function SpinnerLoader(props) {
+ return (
+
+ );
+}
diff --git a/src/components/SpinnerLoader/SpinnerLoader.scss b/src/components/SpinnerLoader/SpinnerLoader.scss
new file mode 100644
index 0000000..d7eb3b0
--- /dev/null
+++ b/src/components/SpinnerLoader/SpinnerLoader.scss
@@ -0,0 +1,9 @@
+.SpinnerLoader {
+ margin: 5rem;
+ height: 10rem;
+ width: 10rem;
+
+ &__wrapper {
+ height: 70vh;
+ }
+}
diff --git a/src/components/SpinnerLoader/index.js b/src/components/SpinnerLoader/index.js
new file mode 100644
index 0000000..feb527a
--- /dev/null
+++ b/src/components/SpinnerLoader/index.js
@@ -0,0 +1 @@
+export { default } from "./SpinnerLoader";
diff --git a/src/constants/routes.js b/src/constants/routes.js
index 8c7af62..65c9da2 100644
--- a/src/constants/routes.js
+++ b/src/constants/routes.js
@@ -1,3 +1,4 @@
+export const BASE_URL = "https://rickandmortyapi.com/api";
export const HOME = "/";
export const EPISODES = "/episodes";
export const EPISODE = "/episode";
diff --git a/src/hocs/index.js b/src/hocs/index.js
new file mode 100644
index 0000000..23fa7ef
--- /dev/null
+++ b/src/hocs/index.js
@@ -0,0 +1 @@
+export { default as withLayout } from "./withLayout";
diff --git a/src/hocs/withLayout.js b/src/hocs/withLayout.js
new file mode 100644
index 0000000..e80d769
--- /dev/null
+++ b/src/hocs/withLayout.js
@@ -0,0 +1,25 @@
+import React, { Component } from "react";
+
+import Header from "../components/Header";
+import Main from "../components/Main";
+import Footer from "../components/Footer";
+
+function withLayout(WrappedComponent) {
+ class Layout extends Component {
+ render() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ }
+
+ return Layout;
+}
+
+export default withLayout;
diff --git a/src/hooks/index.js b/src/hooks/index.js
new file mode 100644
index 0000000..c671f3a
--- /dev/null
+++ b/src/hooks/index.js
@@ -0,0 +1 @@
+export { default as useFetch } from "./useFetch";
diff --git a/src/hooks/useFetch.js b/src/hooks/useFetch.js
new file mode 100644
index 0000000..7eb9695
--- /dev/null
+++ b/src/hooks/useFetch.js
@@ -0,0 +1,34 @@
+import { useReducer } from "react";
+
+const initialState = {
+ hasLoaded: false,
+ hasFailed: false,
+ errorMsg: null,
+ data: null,
+};
+
+const reducer = (state, action) => {
+ const { type, data, errorMsg } = action;
+
+ switch (type) {
+ case "successful":
+ return {
+ ...state,
+ hasLoaded: true,
+ data,
+ };
+ case "failed":
+ return {
+ ...state,
+ hasLoaded: true,
+ hasFailed: true,
+ errorMsg: errorMsg,
+ };
+ default:
+ throw new Error();
+ }
+};
+
+export default function useFetch() {
+ return useReducer(reducer, initialState);
+}
diff --git a/src/images/01-home-start.png b/src/images/01-home-start.png
deleted file mode 100644
index 2c4ed6e..0000000
Binary files a/src/images/01-home-start.png and /dev/null differ
diff --git a/src/images/02-home-disabled-next-page.png b/src/images/02-home-disabled-next-page.png
deleted file mode 100644
index c7e98be..0000000
Binary files a/src/images/02-home-disabled-next-page.png and /dev/null differ
diff --git a/src/images/03-episode-page.png b/src/images/03-episode-page.png
deleted file mode 100644
index 3a96615..0000000
Binary files a/src/images/03-episode-page.png and /dev/null differ
diff --git a/src/images/04-character-page.png b/src/images/04-character-page.png
deleted file mode 100644
index 51c7072..0000000
Binary files a/src/images/04-character-page.png and /dev/null differ
diff --git a/src/images/05-location-page.png b/src/images/05-location-page.png
deleted file mode 100644
index cf4b0ec..0000000
Binary files a/src/images/05-location-page.png and /dev/null differ
diff --git a/src/images/character-page.png b/src/images/character-page.png
new file mode 100644
index 0000000..a018512
Binary files /dev/null and b/src/images/character-page.png differ
diff --git a/src/images/dead.png b/src/images/dead.png
new file mode 100644
index 0000000..3285aa8
Binary files /dev/null and b/src/images/dead.png differ
diff --git a/src/images/episode-page.png b/src/images/episode-page.png
new file mode 100644
index 0000000..22b36c8
Binary files /dev/null and b/src/images/episode-page.png differ
diff --git a/src/images/error-image.png b/src/images/error-image.png
new file mode 100644
index 0000000..073dd81
Binary files /dev/null and b/src/images/error-image.png differ
diff --git a/src/images/heartbeat.png b/src/images/heartbeat.png
new file mode 100644
index 0000000..dabf901
Binary files /dev/null and b/src/images/heartbeat.png differ
diff --git a/src/images/home-start.png b/src/images/home-start.png
new file mode 100644
index 0000000..0a9258c
Binary files /dev/null and b/src/images/home-start.png differ
diff --git a/src/images/location-page.png b/src/images/location-page.png
new file mode 100644
index 0000000..32b9e10
Binary files /dev/null and b/src/images/location-page.png differ
diff --git a/src/images/rick-in-peace.jpg b/src/images/rick-in-peace.jpg
new file mode 100644
index 0000000..2dcd4c9
Binary files /dev/null and b/src/images/rick-in-peace.jpg differ
diff --git a/src/images/wallpaper.jpg b/src/images/wallpaper.jpg
new file mode 100644
index 0000000..9b58f69
Binary files /dev/null and b/src/images/wallpaper.jpg differ
diff --git a/src/pages/Character/Character.class.js b/src/pages/Character/Character.class.js
new file mode 100644
index 0000000..7290435
--- /dev/null
+++ b/src/pages/Character/Character.class.js
@@ -0,0 +1,83 @@
+import { Component } from "react";
+import { fetchDataset, getCharacter } from "../../api/requests";
+
+import { ErrorMessageCard } from "../../components/MessageCard";
+import CharacterProfile from "../../components/CharacterProfile";
+import Divider from "../../components/Divider";
+import EpisodeCard from "../../components/EpisodeCard";
+import EpisodeGrid from "../../components/EpisodeGrid";
+import SpinnerLoader from "../../components/SpinnerLoader";
+import { withLayout } from "../../hocs";
+
+class Character extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ character: null,
+ episodes: [],
+ hasLoaded: false,
+ hasError: false,
+ errorMessage: null,
+ };
+ }
+
+ componentDidMount() {
+ this.loadCharacter();
+ }
+
+ componentDidUpdate(prevProps) {
+ prevProps.id !== this.props.id && this.loadCharacter();
+ }
+
+ loadCharacter = async () => {
+ try {
+ const { id } = this.props;
+ const { data: character, error } = await getCharacter({ id });
+
+ if (error !== null) throw error;
+ const { dataset: episodes } = await fetchDataset(character.episode);
+
+ this.setState((prevState) => ({
+ ...prevState,
+ hasLoaded: true,
+ character: character,
+ episodes: episodes,
+ }));
+ } catch (error) {
+ this.setState((prevState) => ({
+ ...prevState,
+ hasLoaded: true,
+ hasError: true,
+ errorMessage: error.message,
+ }));
+ }
+ };
+
+ render() {
+ const { hasLoaded, hasError, character, episodes } = this.state;
+
+ return (
+ <>
+ {!hasLoaded && }
+ {hasLoaded && hasError && }
+ {hasLoaded && !hasError && (
+ <>
+ {}
+
+
+ Episodes
+
+
+ {episodes.map((episode) => (
+
+ ))}
+
+ >
+ )}
+ >
+ );
+ }
+}
+
+export default withLayout(Character);
diff --git a/src/pages/Character/Character.js b/src/pages/Character/Character.js
new file mode 100644
index 0000000..a1a604c
--- /dev/null
+++ b/src/pages/Character/Character.js
@@ -0,0 +1,64 @@
+import React, { useEffect } from "react";
+import { useFetch } from "../../hooks";
+import { fetchDataset, getCharacter } from "../../api/requests";
+
+import { ErrorMessageCard } from "../../components/MessageCard";
+import CharacterProfile from "../../components/CharacterProfile";
+import Divider from "../../components/Divider";
+import EpisodeCard from "../../components/EpisodeCard";
+import EpisodeGrid from "../../components/EpisodeGrid";
+import SpinnerLoader from "../../components/SpinnerLoader";
+import { withLayout } from "../../hocs";
+
+function Character(props) {
+ const { id } = props;
+ const [{ hasLoaded, hasFailed, data, errorMsg }, fetchDispatch] = useFetch();
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const { data: character, error: errorCharacter } = await getCharacter({ id });
+
+ if (errorCharacter) throw errorCharacter;
+
+ const { dataset: episodes } = await fetchDataset(character.episode);
+
+ fetchDispatch({
+ type: "successful",
+ data: {
+ character,
+ episodes,
+ },
+ });
+ } catch (error) {
+ fetchDispatch({
+ type: "failed",
+ errorMsg: "Something went wrong",
+ });
+ }
+ })();
+ }, [id, fetchDispatch]);
+
+ return (
+ <>
+ {!hasLoaded && }
+ {hasLoaded && hasFailed && }
+ {hasLoaded && !hasFailed && (
+ <>
+ {}
+
+
+ Episodes
+
+
+ {data.episodes.map((episode) => (
+
+ ))}
+
+ >
+ )}
+ >
+ );
+}
+
+export default withLayout(Character);
diff --git a/src/pages/Character/index.js b/src/pages/Character/index.js
new file mode 100644
index 0000000..f267ceb
--- /dev/null
+++ b/src/pages/Character/index.js
@@ -0,0 +1 @@
+export { default } from "./Character";
diff --git a/src/pages/Episode/Episode.class.js b/src/pages/Episode/Episode.class.js
new file mode 100644
index 0000000..e1078af
--- /dev/null
+++ b/src/pages/Episode/Episode.class.js
@@ -0,0 +1,88 @@
+import React, { Component } from "react";
+import { fetchDataset, getEpisode } from "../../api/requests";
+
+import { ErrorMessageCard } from "../../components/MessageCard";
+import CharacterCard from "../../components/CharacterCard";
+import CharacterGrid from "../../components/CharacterGrid";
+import Divider from "../../components/Divider";
+import Flex from "../../components/Flex";
+import SpinnerLoader from "../../components/SpinnerLoader";
+import { withLayout } from "../../hocs";
+
+class Episode extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ episode: null,
+ characters: [],
+ hasLoaded: false,
+ hasError: false,
+ errorMessage: null,
+ };
+ }
+
+ componentDidMount() {
+ this.loadEpisode();
+ }
+
+ componentDidUpdate(prevProps) {
+ prevProps.id !== this.props.id && this.loadEpisode();
+ }
+
+ loadEpisode = async () => {
+ try {
+ const { id } = this.props;
+ const { data: episode, error } = await getEpisode({ id });
+
+ if (error) throw error;
+
+ const { dataset: characters } = await fetchDataset(episode.characters);
+
+ this.setState((prevState) => ({
+ ...prevState,
+ hasLoaded: true,
+ episode: episode,
+ characters: characters,
+ }));
+ } catch (error) {
+ this.setState((prevState) => ({
+ ...prevState,
+ hasLoaded: true,
+ hasError: true,
+ errorMessage: error.message,
+ }));
+ }
+ };
+
+ render() {
+ const { hasLoaded, hasError, episode, characters } = this.state;
+
+ return (
+ <>
+ {!hasLoaded && }
+ {hasLoaded && hasError && }
+ {hasLoaded && !hasError && (
+ <>
+
+ {episode.name}
+
+ {episode.episode} | {episode.air_date}
+
+
+
+ Characters that appeared
+
+
+ {characters.map((character) => (
+
+ ))}
+
+ >
+ )}
+ >
+ );
+ }
+}
+
+export default withLayout(Episode);
diff --git a/src/pages/Episode/Episode.js b/src/pages/Episode/Episode.js
index 8255df5..ed81970 100644
--- a/src/pages/Episode/Episode.js
+++ b/src/pages/Episode/Episode.js
@@ -1,42 +1,68 @@
-import React, { Component } from "react";
-
-import Layout from "../../components/Layout";
-// import CharacterCard from "../../components/CharacterCard";
-
-class Episode extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- // episode: null,
- // characters: [],
- // hasLoaded: false,
- // hasError: false,
- // errorMessage: null,
- }
-
- render() {
- return (
-
-
-
- {/* {characters.map((character) => (
-
- ))} */}
-
-
-
- );
- }
+import React, { useEffect } from "react";
+import { useFetch } from "../../hooks";
+import { fetchDataset, getEpisode } from "../../api/requests";
+
+import { ErrorMessageCard } from "../../components/MessageCard";
+import CharacterCard from "../../components/CharacterCard";
+import CharacterGrid from "../../components/CharacterGrid";
+import Divider from "../../components/Divider";
+import Flex from "../../components/Flex";
+import SpinnerLoader from "../../components/SpinnerLoader";
+import { withLayout } from "../../hocs";
+
+function Episode(props) {
+ const { id } = props;
+ const [{ hasLoaded, hasFailed, data, errorMsg }, fetchDispatch] = useFetch();
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const { data: episode, error: errorEpisode } = await getEpisode({ id });
+
+ if (errorEpisode) throw errorEpisode;
+
+ const { dataset: characters } = await fetchDataset(episode.characters);
+
+ fetchDispatch({
+ type: "successful",
+ data: {
+ episode,
+ characters,
+ },
+ });
+ } catch (error) {
+ fetchDispatch({
+ type: "failed",
+ errorMsg: "Something went wrong",
+ });
+ }
+ })();
+ }, [id, fetchDispatch]);
+
+ return (
+ <>
+ {!hasLoaded && }
+ {hasLoaded && hasFailed && }
+ {hasLoaded && !hasFailed && (
+ <>
+
+ {data.episode.name}
+
+ {data.episode.episode} | {data.episode.air_date}
+
+
+
+ Characters that appeared
+
+
+ {data.characters.map((character) => (
+
+ ))}
+
+ >
+ )}
+ >
+ );
}
-export default Episode;
+export default withLayout(Episode);
diff --git a/src/pages/Home/Home.class.js b/src/pages/Home/Home.class.js
new file mode 100644
index 0000000..8122721
--- /dev/null
+++ b/src/pages/Home/Home.class.js
@@ -0,0 +1,97 @@
+import React, { Component } from "react";
+import { getEpisodes } from "../../api/requests";
+
+import { ButtonLink } from "../../components/Button";
+import { ErrorMessageCard } from "../../components/MessageCard";
+import Divider from "../../components/Divider";
+import EpisodeCard from "../../components/EpisodeCard";
+import EpisodeGrid from "../../components/EpisodeGrid";
+import Flex from "../../components/Flex";
+import SpinnerLoader from "../../components/SpinnerLoader";
+import withLayout from "../../hocs/withLayout";
+
+import styled from "styled-components";
+
+const Title = styled.h1`
+ font-size: 2.5rem;
+ text-align: center;
+`;
+
+class Home extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ paginationInfo: null,
+ episodes: [],
+ hasLoaded: false,
+ hasError: false,
+ errorMessage: null,
+ };
+ }
+
+ componentDidMount() {
+ this.loadEpisodes();
+ }
+
+ componentDidUpdate(prevProps) {
+ prevProps.page !== this.props.page && this.loadEpisodes();
+ }
+
+ loadEpisodes = async () => {
+ try {
+ const { page } = this.props;
+ const { data, error } = await getEpisodes({ page });
+
+ if (error) throw error;
+
+ this.setState((prevState) => ({
+ ...prevState,
+ hasLoaded: true,
+ episodes: data.results,
+ paginationInfo: data.info,
+ }));
+ } catch (error) {
+ this.setState((prevState) => ({
+ ...prevState,
+ hasLoaded: true,
+ hasError: true,
+ errorMessage: error,
+ }));
+ }
+ };
+
+ render() {
+ const { page } = this.props;
+ const { hasLoaded, hasError, episodes, paginationInfo } = this.state;
+
+ return (
+ <>
+ Episodes
+ {!hasLoaded && }
+ {hasLoaded && hasError && }
+ {hasLoaded && !hasError && (
+ <>
+
+
+ {episodes.map((episode) => (
+
+ ))}
+
+
+
+
+ Previous
+
+
+ Next
+
+
+ >
+ )}
+ >
+ );
+ }
+}
+
+export default withLayout(Home);
diff --git a/src/pages/Home/Home.js b/src/pages/Home/Home.js
index f86e93c..6b1f0c3 100644
--- a/src/pages/Home/Home.js
+++ b/src/pages/Home/Home.js
@@ -1,57 +1,76 @@
-import React, { Component } from "react";
-
-import Layout from "../../components/Layout";
-// import EpisodeCard from "../../components/EpisodeCard";
-
-class Home extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- // page: 1,
- // paginationInfo: null,
- // episodes: [],
- // hasLoaded: false,
- // hasError: false,
- // errorMessage: null,
- }
-
- async componentDidMount() {
- // this.loadEpisodes();
- }
-
- async loadEpisodes() {
- console.log(this);
- }
-
- render() {
- return (
-
-
- {/* {hasLoaded && !hasError && (
-
-
Episodes loaded!
-
- )} */}
-
-
-
- {/* {episodes.map((episode) => (
-
- ))} */}
-
-
-
-
-
- );
- }
+import React, { useEffect } from "react";
+import { useFetch } from "../../hooks";
+import { getEpisodes } from "../../api/requests";
+
+import { ButtonLink } from "../../components/Button";
+import { ErrorMessageCard } from "../../components/MessageCard";
+import Divider from "../../components/Divider";
+import EpisodeCard from "../../components/EpisodeCard";
+import EpisodeGrid from "../../components/EpisodeGrid";
+import Flex from "../../components/Flex";
+import SpinnerLoader from "../../components/SpinnerLoader";
+import withLayout from "../../hocs/withLayout";
+
+import styled from "styled-components";
+
+const Title = styled.h1`
+ font-size: 2.5rem;
+ text-align: center;
+`;
+
+function Home(props) {
+ const { page } = props;
+ const [{ hasLoaded, hasFailed, data, errorMsg }, fetchDispatch] = useFetch();
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const { data, error } = await getEpisodes({ page });
+
+ if (error) throw error;
+
+ fetchDispatch({
+ type: "successful",
+ data: {
+ info: data.info,
+ episodes: data.results,
+ },
+ });
+ } catch (error) {
+ fetchDispatch({
+ type: "failed",
+ errorMsg: "Something went wrong",
+ });
+ }
+ })();
+ }, [page, fetchDispatch]);
+
+ return (
+ <>
+ Episodes
+ {!hasLoaded && }
+ {hasLoaded && hasFailed && }
+ {hasLoaded && !hasFailed && (
+ <>
+
+
+ {data.episodes.map((episode) => (
+
+ ))}
+
+
+
+
+ Previous
+
+
+ Next
+
+
+ >
+ )}
+ >
+ );
}
-export default Home;
+export default withLayout(Home);
diff --git a/src/pages/Location/Location.class.js b/src/pages/Location/Location.class.js
new file mode 100644
index 0000000..b1f3364
--- /dev/null
+++ b/src/pages/Location/Location.class.js
@@ -0,0 +1,91 @@
+import React, { Component } from "react";
+import { fetchDataset, getLocation } from "../../api/requests";
+
+import { ErrorMessageCard, NoResidentsCard } from "../../components/MessageCard";
+import CharacterCard from "../../components/CharacterCard";
+import CharacterGrid from "../../components/CharacterGrid";
+import Divider from "../../components/Divider";
+import Flex from "../../components/Flex";
+import SpinnerLoader from "../../components/SpinnerLoader";
+import { withLayout } from "../../hocs";
+
+class Location extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ location: null,
+ characters: [],
+ hasLoaded: false,
+ hasError: false,
+ errorMessage: null,
+ };
+ }
+
+ componentDidMount() {
+ this.loadLocation();
+ }
+
+ componentDidUpdate(prevProps) {
+ prevProps.id !== this.props.id && this.loadLocation();
+ }
+
+ loadLocation = async () => {
+ try {
+ const { id } = this.props;
+ const { data: location, error } = await getLocation({ id });
+
+ if (error) throw error;
+
+ const { dataset: characters } = await fetchDataset(location.residents);
+
+ this.setState((prevState) => ({
+ ...prevState,
+ hasLoaded: true,
+ location: location,
+ characters: characters,
+ }));
+ } catch (error) {
+ this.setState((prevState) => ({
+ ...prevState,
+ hasLoaded: true,
+ hasError: true,
+ errorMessage: error.message,
+ }));
+ }
+ };
+
+ render() {
+ const { hasLoaded, hasError, location, characters } = this.state;
+
+ return (
+ <>
+ {!hasLoaded && }
+ {hasLoaded && hasError && }
+ {hasLoaded && !hasError && (
+ <>
+
+ {location.name}
+
+ {location.type} at {location.dimension}
+
+
+
+ Resident characters
+
+ {Boolean(characters.length) && (
+
+ {characters.map((character) => (
+
+ ))}
+
+ )}
+ {Boolean(!characters.length) && }
+ >
+ )}
+ >
+ );
+ }
+}
+
+export default withLayout(Location);
diff --git a/src/pages/Location/Location.js b/src/pages/Location/Location.js
new file mode 100644
index 0000000..f60aec9
--- /dev/null
+++ b/src/pages/Location/Location.js
@@ -0,0 +1,71 @@
+import React, { useEffect } from "react";
+import { withLayout } from "../../hocs";
+import { useFetch } from "../../hooks";
+import { fetchDataset, getLocation } from "../../api/requests";
+
+import { ErrorMessageCard, NoResidentsCard } from "../../components/MessageCard";
+import CharacterCard from "../../components/CharacterCard";
+import CharacterGrid from "../../components/CharacterGrid";
+import Divider from "../../components/Divider";
+import Flex from "../../components/Flex";
+import SpinnerLoader from "../../components/SpinnerLoader";
+
+function Location(props) {
+ const { id } = props;
+ const [{ hasLoaded, hasFailed, data, errorMsg }, fetchDispatch] = useFetch();
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const { data: location, error: errorLocation } = await getLocation({ id });
+
+ if (errorLocation) throw errorLocation;
+
+ const { dataset: characters } = await fetchDataset(location.residents);
+
+ fetchDispatch({
+ type: "successful",
+ data: {
+ location,
+ characters,
+ },
+ });
+ } catch (error) {
+ fetchDispatch({
+ type: "failed",
+ errorMsg: "Something went wrong",
+ });
+ }
+ })();
+ }, [id, fetchDispatch]);
+
+ return (
+ <>
+ {!hasLoaded && }
+ {hasLoaded && hasFailed && }
+ {hasLoaded && !hasFailed && (
+ <>
+
+ {data.location.name}
+
+ {data.location.type} at {data.location.dimension}
+
+
+
+ Resident characters
+
+ {Boolean(data.characters.length) && (
+
+ {data.characters.map((character) => (
+
+ ))}
+
+ )}
+ {Boolean(!data.characters.length) && }
+ >
+ )}
+ >
+ );
+}
+
+export default withLayout(Location);
diff --git a/src/pages/Location/index.js b/src/pages/Location/index.js
new file mode 100644
index 0000000..7625c87
--- /dev/null
+++ b/src/pages/Location/index.js
@@ -0,0 +1 @@
+export { default } from "./Location";
diff --git a/src/theme/FontStyle.js b/src/theme/FontStyle.js
new file mode 100644
index 0000000..349d817
--- /dev/null
+++ b/src/theme/FontStyle.js
@@ -0,0 +1,9 @@
+import { createGlobalStyle } from "styled-components";
+
+const FontStyle = createGlobalStyle`
+ body {
+ font-family: 'Oswald', sans-serif;
+ }
+`;
+
+export { FontStyle };
diff --git a/src/theme/GlobalStyle.js b/src/theme/GlobalStyle.js
new file mode 100644
index 0000000..d06a95c
--- /dev/null
+++ b/src/theme/GlobalStyle.js
@@ -0,0 +1,56 @@
+import { createGlobalStyle } from "styled-components";
+
+const GlobalStyle = createGlobalStyle`
+ body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ }
+
+ * {
+ box-sizing: border-box;
+ }
+
+ ul,
+ ol {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ a {
+ color: inherit;
+ text-decoration: none;
+ }
+
+
+ *::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ *::-webkit-scrollbar-thumb {
+ background: rgb(204, 204, 204);
+ border-radius: 4px;
+ }
+
+ *::-webkit-scrollbar-thumb:hover {
+ background: rgb(179, 179, 179);
+ box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.2);
+ }
+
+ *::-webkit-scrollbar-thumb:active {
+ background-color: rgb(153, 153, 153);
+ }
+
+ *::-webkit-scrollbar-track {
+ background: rgb(224, 224, 224);
+ border-radius: 4px;
+ }
+
+ *::-webkit-scrollbar-track:hover,
+ *::-webkit-scrollbar-track:active {
+ background: rgb(212, 212, 212);
+ }
+`;
+
+export { GlobalStyle };
diff --git a/src/theme/index.js b/src/theme/index.js
new file mode 100644
index 0000000..810b934
--- /dev/null
+++ b/src/theme/index.js
@@ -0,0 +1,3 @@
+export { default } from "./theme";
+export { GlobalStyle } from "./GlobalStyle";
+export { FontStyle } from "./FontStyle";
diff --git a/src/theme/theme.js b/src/theme/theme.js
new file mode 100644
index 0000000..d3a2660
--- /dev/null
+++ b/src/theme/theme.js
@@ -0,0 +1,24 @@
+const theme = {
+ breakpoints: {
+ xs: "480px",
+ sm: "576px",
+ md: "768px",
+ lg: "992px",
+ xl: "1200px",
+ },
+ font: {
+ primary: "'Oswald', sans-serif",
+ secondary: "'Montserrat', sans-serif",
+ },
+ palette: {
+ dark: {
+ main: "rgb(16,16,16)",
+ secondary: "rgb(32,32,32)",
+ contrast: "#e0e0e0",
+ hover: "rgb(64, 64, 64)",
+ contrastHover: "rgb(224, 224, 224)",
+ },
+ },
+};
+
+export default theme;