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} + + {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}
-