diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..91e54bf8 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": [["styled-components", { "ssr": true }]] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ee9613cb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Elison Michell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d63e8568..da46b704 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,102 @@ -# **TESTE DE FRONTEND** + -Neste teste, você será livre para criar uma aplicação consumindo a API que você quiser e com o tema que desejar. -Contudo, o seu projeto deverá seguir os requisitos mínimos de conteúdo. + ![file cover - 1](https://user-images.githubusercontent.com/97061419/212690817-67ad2e1a-f067-4b43-9260-57a44f6e5421.png) + -SUGESTÕES DE APIS: -https://github.com/public-apis/public-apis +
+ GitHub repo size + GitHub language count + GitHub license + Status + + Link + +
---------------------------------------------------------------------- -## REQUISITOS: +## 📚 About -## GIT -- Faça um fork deste repositório. -- Criar uma branch para codar as suas features. -- Criar um pull-request quando o teste for finalizado e submetido. +

📌 Project developed for the technical test of Orma Carbon.

+

+ Pokémon Universe is a pokedex made for listing pokémons using the PokéAPI to capture information about pokémons. +

-##### **NOTA: Será avaliado também se o nome da branch, títulos de commit, push e comentários possuem boa legibilidade.** +## 🌦 Orma Carbon ------------------------------------------------------ + - At Orma Carbon, innovative solutions are created for climate action, emissions compensation and access to the carbon market. + - Our solutions range from accessing our projects and credits, to portfolio creation and ESG instruments for purpose-built companies and investors. + - Real-time emissions calculation and offsetting within service and e-commerce platforms – affordable and convenient for users, at no cost to platforms. + + Check out more about Orma Carbon's services here! -## FRAMEWORK +## 📦 Features -- Utilizar as ferramentas presentes no framework do projeto (NEXT.JS). +- [x] Animated video as banner +- [x] Navigation menu +- [x] Pokémon search bar +- [x] Theme switching by context API +- [x] Listing with detailed information of each pokémon released +- [x] Interactive status chart +- [x] Fully responsive and mobile friendly website ------------------------------------------------------ +## 🛠 Tech Stack -## ESTILOS +- HTML5 +- CSS3 +- Javascript +- NextJS +- Styled Components +- Axios +- REST api +- Context api +- Phosphor icons -- Os estilos deste teste devem ser feitos em styled-components (evite utilizar bootstrap, mas se necessário, use). -- O projeto deverá conter tema claro/escuro e forma do usuário alterar entre os dois. -- Deve ser totalmente responsivo. +## 🎨 Gallery ------------------------------------------------------ +
+ + ![localhost_3000_overview (15)](https://user-images.githubusercontent.com/97061419/212695415-a0e54028-6f40-4b74-b1bc-98ae36d7588c.png) + ![localhost_3000_overview (3)](https://user-images.githubusercontent.com/97061419/212695697-7ba6145e-eb20-4e2b-b468-fec2e4b96139.png) + ![localhost_3000_overview (6)](https://user-images.githubusercontent.com/97061419/212695793-25fc18fa-bd36-499d-99d0-3a0952cebeb4.png) + ![localhost_3000_overview (13)](https://user-images.githubusercontent.com/97061419/212695588-71ab4eea-ffd0-4f58-8eda-3573af7d7c92.png) + ![localhost_3000_overview (2)](https://user-images.githubusercontent.com/97061419/212696122-e89d63f5-d1e3-419d-8cf8-2d3055bb28cb.png) + ![localhost_3000_overview (7)](https://user-images.githubusercontent.com/97061419/212696085-d9eb6fd0-f527-454e-a86e-6179f1fa8acd.png) -## PROJETO +
-- Deve utilizar useContext, useState e useEffect. -- Ter ao menos 3 paginas navegáveis com router (ex: um navbar para facilitar a navegação). -- Deve consumir uma API de sua escolha, desde que os dados sejam filtraveis e paginados. No mínimo 15 itens por requisição. +## 🚀 Getting Started -------------------------------------------------------- +

These instructions will get you a copy of the full project up and running on your local machine for development and testing purposes.

+

The project can be built with npm or yarn, so choose one of the approach bellow in case you don't have any installed on your system.

-## REQUISITOS DIFERENCIAIS: +- Node.js +- Yarn +- Git -- Código tentando seguir SOLID. -- Código performático. -- Utilizar inglês no projeto todo. -- Utilizar Injeção de Dependências. -- Fazer deploy do mesmo (heroku, netlify, aws, vercel, github pages ou outro da preferência). +## ⚙️ Setting up + +```bash +# Clone this repository +$ git clone https://github.com/pokemon-universe.git + +# Go into the repository +$ cd pokemon-universe + +# Switch branch +$ git checkout pokedex-develop-branch + +# Install dependencies +$ npm install + +# start the application +$ npm run dev + +# Now you have a copy of the project to modify and test in your editor. +``` + +## 📝 License + +This project is under the MIT license. See the LICENSE for more information. + +## + +Made with ♥ by Elison Michell 👋 Get in touch! diff --git a/components/AttributeBox/index.js b/components/AttributeBox/index.js new file mode 100644 index 00000000..89cdc76b --- /dev/null +++ b/components/AttributeBox/index.js @@ -0,0 +1,25 @@ +import React from 'react' +import TypeLabel from '../TypeLabel'; +import { Container, Content, Types } from './styles' + +function AttributeBox(props) { + return( + +

Attributes

+ + + + { + props.types.map((item) => ( + + )) + } + + +
+ ) +} +export default AttributeBox; \ No newline at end of file diff --git a/components/AttributeBox/styles.js b/components/AttributeBox/styles.js new file mode 100644 index 00000000..238d87da --- /dev/null +++ b/components/AttributeBox/styles.js @@ -0,0 +1,39 @@ +import styled from 'styled-components' + +export const Container = styled.div` + flex: 1; + display: flex; + flex-direction: column; + padding: ${(props) => props.theme.spacing.md}; + border: 1px solid ${(props) => props.theme.colors.border}; + border-radius: ${(props) => props.theme.spacing.md}; + transition: background 0.3s, color 0.3s, border 0.3s; + color: ${props => props.theme.colors.text} + + h3 { + margin-bottom: ${(props) => props.theme.spacing.lg}; + } + + li { + list-style: none; + } + + ul { + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.spacing.xs}; + } +` +export const Content = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +` + +export const Types = styled.div` + display: flex; + height: 100%; + flex-direction: column; + justify-content: flex-end; + gap: ${(props) => props.theme.spacing.sm}; +` \ No newline at end of file diff --git a/components/Box/index.js b/components/Box/index.js new file mode 100644 index 00000000..d97f667d --- /dev/null +++ b/components/Box/index.js @@ -0,0 +1,17 @@ +import React from 'react' +import { Container } from './styles' + +function Box({title, children}) { + return( + + {title && +
+

{title}

+
+ } + + {children} +
+ ) +} +export default Box; \ No newline at end of file diff --git a/components/Box/styles.js b/components/Box/styles.js new file mode 100644 index 00000000..83304219 --- /dev/null +++ b/components/Box/styles.js @@ -0,0 +1,23 @@ +import styled from 'styled-components' + +export const Container = styled.section` + flex: 1; + display: flex; + flex-direction: column; + background: ${props => props.theme.colors.text+10}; + padding: ${props => props.theme.spacing.xl}; + border-radius: ${props => props.theme.spacing.xl}; + border: 1px solid ${ props => props.theme.colors.border}; + transition: background 0.3s, color 0.3s, border 0.3s; + + .title { + font-size: ${props => props.theme.fonts.size.lg}; + font-weight: 600; + margin-bottom: ${props => props.theme.spacing.lg}; + color: ${props => props.theme.colors.text}; + } + + @media (max-width: ${props => props.theme.breakpoints.sm}) { + padding: ${props => props.theme.spacing.xl} ${props => props.theme.spacing.lg} ${props => props.theme.spacing.lg}; + } +` \ No newline at end of file diff --git a/components/Button/index.js b/components/Button/index.js new file mode 100644 index 00000000..1824c74e --- /dev/null +++ b/components/Button/index.js @@ -0,0 +1,12 @@ +import React from 'react' +import { Container } from './styles' + +function Button(props) { + return( + + {props.icon} + {props.title} + + ) +} +export default Button; \ No newline at end of file diff --git a/components/Button/styles.js b/components/Button/styles.js new file mode 100644 index 00000000..79492f70 --- /dev/null +++ b/components/Button/styles.js @@ -0,0 +1,43 @@ +import styled from "styled-components"; + +export const Container = styled.button` + display: flex; + align-items: center; + gap: ${props => (props.title && props.icon) && props.theme.spacing.md}; + width: ${props => props.flex ? '100%' : 'max-content'}; + min-width: ${props => !props.icon && '10rem'}; + border-radius: ${props => (props.rounded && '50%') || props.theme.spacing.md}; + padding: ${props => props.theme.spacing[props.size] || props.theme.spacing.md}; + font-size: ${props => props.theme.fonts.size.md}; + font-weight: 600; + cursor: pointer; + + color: ${ + props => props => (props.active && + props.theme.colors.white) || + props.theme.colors[props.textColor] || + props.theme.colors.text}; + + background: ${ + props => props.active ? + props.theme.colors[props.activeColor] || + props.theme.colors.primary100 : + props.theme.colors[props.color] || + props.theme.colors.primary200 + }; + transition: background 0.3s, color 0.3s; + + span { + text-align: ${props => props.icon && 'start'}; + flex:1; + } + + :hover { + background: ${ props => !props.active && (props.theme.colors[props.hoverColor] || props.theme.colors.text+20) }; + } + + :disabled { + pointer-events: none; + filter: opacity(0.5); + } +` diff --git a/components/Card/index.js b/components/Card/index.js new file mode 100644 index 00000000..6b722b1a --- /dev/null +++ b/components/Card/index.js @@ -0,0 +1,37 @@ +import React, { useContext } from "react"; +import { Container, Content, Header, Types } from "./styles"; +import { RouteContext } from "../../context/routeContext"; + +import TypeLabel from "../TypeLabel"; +import Frame from "../Frame"; + +function Card(props) { + const { toPokemonLink } = useContext(RouteContext); + + return ( + toPokemonLink(props.name)}> + {props.sprite && ( + + )} + +
+
Nº {props.id.toString().padStart(3, "0")}
+ {props.name} +
+ + {props.type && + props.type.map((type, id) => ( + + ))} + +
+
+ ); +} +export default Card; diff --git a/components/Card/styles.js b/components/Card/styles.js new file mode 100644 index 00000000..5a33e16f --- /dev/null +++ b/components/Card/styles.js @@ -0,0 +1,68 @@ +import styled from "styled-components"; + +export const Container = styled.button` + flex: 1; + display: flex; + background: ${(props) => props.theme.colors.text+10}; + flex-direction: column; + gap: ${(props) => props.theme.spacing.sm}; + padding: ${(props) => props.theme.spacing.md}; + border-radius: ${(props) => props.theme.spacing.md}; + color: ${(props) => props.theme.colors.text}; + border: 1px solid ${(props) => props.theme.colors.border}; + transition: background 0.3s, border 0.3s, color 0.3s, margin 0.3s; + margin: ${(props) => props.theme.spacing.sm}; + cursor: pointer; + + :hover { + margin: 0; + } + + :active { + background: transparent; + } + + @media (max-width: ${props => props.theme.breakpoints.sm}) { + width: 100%; + } +` + +export const Header = styled.header` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.spacing.xs}; + text-align: start; + font-weight: 600; + + span { + text-transform: capitalize; + font-size: ${(props) => props.theme.fonts.size.xl}; + } + + div { + color: ${(props) => props.theme.colors.gray300}; + border-radius: ${(props) => props.theme.spacing.lg}; + font-size: ${(props) => props.theme.fonts.size.md}; + } +` + +export const Content = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.spacing.sm}; + + .button-container { + flex: 1; + display: flex; + justify-content: flex-end; + align-items: flex-end; + } +` + +export const Types = styled.div` + display: flex; + height: 100%; + gap: ${(props) => props.theme.spacing.xs}; +` diff --git a/components/Chart/index.js b/components/Chart/index.js new file mode 100644 index 00000000..6593a937 --- /dev/null +++ b/components/Chart/index.js @@ -0,0 +1,42 @@ +import React, { useEffect, useState } from 'react' +import { Container, Content, Area, Label, Bar } from './styles' + +function Chart(props) { + const [maxValue, setMaxValue] = useState(0); + + useEffect(()=> { + if(props.data.length > 0) { + const valueList = props.data.map((item) => {return(item.value)}) + const max = Math.max(valueList[0],valueList[1],valueList[2],valueList[3],valueList[4],valueList[5], 100); + setMaxValue(max); + } + },[props]) + + function calcSize(value) { + return Math.min(value/maxValue*100, 100); + } + + return( + +

Stats

+ + + + { + props.data && + props.data.map((item) => { + return ( + + ) + }) + } + + +
+ ) +} +export default Chart; \ No newline at end of file diff --git a/components/Chart/styles.js b/components/Chart/styles.js new file mode 100644 index 00000000..ae75ccd4 --- /dev/null +++ b/components/Chart/styles.js @@ -0,0 +1,121 @@ +import styled, { keyframes } from 'styled-components' + +const heightAnimation = keyframes` + from { + height: 0%; + } + to { + height: ${(props) => props.size}%; + } +`; + +const fadeAnimation = keyframes` + from { + filter: opacity(0) + } + to { + filter: opacity(100) + } +`; + +export const Container = styled.div` + flex: 1; + display: flex; + min-height: 20rem; + flex-direction: column; + gap: ${(props) => props.theme.spacing.md}; + border: 1px solid ${(props) => props.theme.colors.border}; + padding: ${(props) => props.theme.spacing.md} ${(props) => props.theme.spacing.md} ${(props) => props.theme.spacing['3xl']}; + border-radius: ${(props) => props.theme.spacing.md}; + transition: background 0.3s, color 0.3s, border 0.3s; +` + +export const Content = styled.div` + display: flex; + flex: 1; + gap: ${(props) => props.theme.spacing.md}; +` + +export const Label = styled.div` + display: flex; + min-width: 2rem; + flex-direction: column; + align-items: center; + font-size: ${props => props.theme.fonts.size.sm}; + color: ${(props) => props.theme.colors.gray200}; + justify-content: space-between; +` + +export const Area = styled.div` + flex: 1; + position: relative; + display: flex; + justify-content: space-evenly; + align-items: flex-end; + gap: ${(props) => props.theme.spacing.md}; + border: 1px solid ${(props) => props.theme.colors.border}; + padding: ${(props) => props.theme.spacing.md} ${(props) => props.theme.spacing.md} 0; + border-radius: ${(props) => props.theme.spacing.sm}; + transition: color 0.3s, border 0.3s; + + &::before { + content: ""; + position: absolute; + top: 50%; + left: 0; + width: 100%; + height: 1px; + background: ${(props) => props.theme.colors.border}; + transition: background 0.3s; + } +` + +export const Bar = styled.div` + flex: 1; + max-width: 3rem; + position: relative; + animation: ${heightAnimation} 1s ease-in-out; + animation-fill-mode: forwards; + animation-iteration-count: 1; + height: ${(props) => props.size}%; + border-radius: ${(props) => props.theme.spacing.xs}; + background: linear-gradient( ${(props) => props.theme.colors.primary200}, ${(props) => props.theme.colors.primary300}); + transition: linear-gradient 1s; + font-weight: 500; + + :hover { + background: linear-gradient( ${(props) => props.theme.colors.primary100}, ${(props) => props.theme.colors.primary200}); + + &::after { + content: "${(props) => props.value}"; + position: absolute; + width: 100%; + bottom: 0; + padding-bottom: ${(props) => props.theme.spacing.sm}; + font-size: ${props => props.theme.fonts.size.sm}; + text-align: center; + color: ${(props) => props.theme.colors.white}; + animation: ${fadeAnimation} 0.5s ease-in-out; + transition: color 0.3s; + animation-fill-mode: forwards; + animation-iteration-count: 1; + } + } + + &::before { + content: "${(props) => props.label}"; + position: absolute; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; + padding-top: ${(props) => props.theme.spacing.xs}; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + color: ${props => props.theme.colors.text}; + transition: color 0.3s; + bottom: 0; + transform: translateY(100%); + } +` \ No newline at end of file diff --git a/components/ExpandedCard/index.js b/components/ExpandedCard/index.js new file mode 100644 index 00000000..6f62d4e8 --- /dev/null +++ b/components/ExpandedCard/index.js @@ -0,0 +1,52 @@ +import React, { useContext } from "react"; +import { Container, Content, Header, Types, Description } from "./styles"; +import { RouteContext } from "../../context/routeContext"; + +import TypeLabel from "../TypeLabel"; +import Button from "../Button"; +import Frame from "../Frame"; + +function ExpandedCard(props) { + const { toPokemonLink } = useContext(RouteContext); + + return ( + + {props.sprite && ( + + )} + +
+
+

+ {props.name} Nº {props.id.toString().padStart(3, "0")} +

+
+ + {props.type && + props.type.map((type, id) => ( + + ))} + +
+ {props.description} +
+
+
+
+ ); +} +export default ExpandedCard; diff --git a/components/ExpandedCard/styles.js b/components/ExpandedCard/styles.js new file mode 100644 index 00000000..2b3fb518 --- /dev/null +++ b/components/ExpandedCard/styles.js @@ -0,0 +1,71 @@ +import styled from "styled-components"; + +export const Container = styled.div` + flex: 1; + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.spacing.sm}; + padding: ${(props) => props.theme.spacing.md}; + border-radius: ${(props) => props.theme.spacing.xl}; + color: ${(props) => props.theme.colors.text}; + border: 1px solid ${(props) => props.theme.colors.border}; + transition: background 0.3s, border 0.3s; + font-weight: 600; +` + +export const Header = styled.header` + min-height: 3.5rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: ${(props) => props.theme.spacing.xs}; + + .title-container { + display: flex; + gap: ${(props) => props.theme.spacing.sm}; + font-size: ${(props) => props.theme.fonts.size["2xl"]}; + transition: background 0.3s, color 0.3s, border 0.3s; + + p { + text-align: start; + text-transform: capitalize; + color: ${(props) => props.theme.colors.text}; + } + + span { + color: ${(props) => props.theme.colors.gray300}; + border-radius: ${(props) => props.theme.spacing.lg}; + } + } +` + +export const Content = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.spacing.sm}; + + .button-container { + flex: 1; + display: flex; + justify-content: flex-end; + align-items: flex-end; + } +` + +export const Types = styled.div` + display: flex; + justify-content: center; + height: 100%; + flex-direction: column; + gap: ${(props) => props.theme.spacing.xs}; +` + +export const Description = styled.div` + height: 3.75rem; + overflow: hidden; + text-overflow: ellipsis; + text-align: start; + font-size: ${(props) => props.theme.fonts.size.md}; +` diff --git a/components/Frame/index.js b/components/Frame/index.js new file mode 100644 index 00000000..ff331f3b --- /dev/null +++ b/components/Frame/index.js @@ -0,0 +1,19 @@ +import React from 'react' +import Image from 'next/image' +import { Container } from './styles' + +function Frame(props) { + return( + + {props.alt} + + ) +} +export default Frame; \ No newline at end of file diff --git a/components/Frame/styles.js b/components/Frame/styles.js new file mode 100644 index 00000000..1c7babee --- /dev/null +++ b/components/Frame/styles.js @@ -0,0 +1,17 @@ +import styled from 'styled-components' + +export const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + border-radius: ${(props) => props.theme.spacing.md}; + background: ${(props) => props.theme.colors.background}; + border: 1px solid ${(props) => props.theme.colors.border}; + transition: background 0.3s, border 0.3s; + img { + width: 100%; + height: auto; + max-width: 475px; + } +` \ No newline at end of file diff --git a/components/Header/index.js b/components/Header/index.js new file mode 100644 index 00000000..559a53eb --- /dev/null +++ b/components/Header/index.js @@ -0,0 +1,30 @@ +import React, { useContext } from 'react'; +import { Container, Title } from './styles'; +import { RouteContext } from '../../context/routeContext'; +import { ArrowLeft } from 'phosphor-react'; +import Button from '../Button'; +import Input from '../Input'; + +function Header(props) { + const { backRoute } = useContext(RouteContext); + + return( + +
+ { + !props.hideButton && +
+ +
+ ) +} +export default Header; \ No newline at end of file diff --git a/components/Header/styles.js b/components/Header/styles.js new file mode 100644 index 00000000..2629584f --- /dev/null +++ b/components/Header/styles.js @@ -0,0 +1,40 @@ +import styled from 'styled-components' + +export const Container = styled.header` + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: ${props => props.theme.spacing.lg}; + + .left-content { + flex: 1; + display: flex; + gap: ${props => props.theme.spacing.lg}; + align-items: center; + } + + @media (max-width: ${props => props.theme.breakpoints.md}) { + flex-direction: column-reverse; + align-items: flex-start; + gap: ${props => props.theme.spacing['3xl']}; + } +` + +export const Title = styled.header` + text-transform: capitalize; + flex: 1; + + h1 { + font-size: ${props => props.theme.fonts.size['3xl']}; + white-space: nowrap; + } + + h2 { + font-size: ${props => props.theme.fonts.size.lg}; + font-weight: 500; + } + + span { + color: ${props => props.theme.colors.gray300}; + } +` \ No newline at end of file diff --git a/components/Input/index.js b/components/Input/index.js new file mode 100644 index 00000000..a8364aa1 --- /dev/null +++ b/components/Input/index.js @@ -0,0 +1,36 @@ +import React, { useContext, useCallback } from 'react' +import { Container } from './styles' +import { MagnifyingGlass } from 'phosphor-react'; +import Button from '../Button' +import {DatalistInput, startsWithValueFilter } from 'react-datalist-input'; +import { PokemonContext } from '../../context/pokemonContext' +import { RouteContext } from '../../context/routeContext'; + +function Input() { + const { referenceList } = useContext(PokemonContext); + const limitOptionsFilter = useCallback((items) => items.slice(0, 5), []); + const filters = [startsWithValueFilter, limitOptionsFilter]; + const { toPokemonLink } = useContext(RouteContext); + + function handleSubmit(event) { + event.preventDefault(); + toPokemonLink(event.target.elements.pokemon.value); + } + + return( + handleSubmit(event)}> +