From a55981426085b1ffb5eec377ad31584bfc90144d Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Thu, 26 Jan 2023 22:31:47 +0800 Subject: [PATCH 01/13] Update PokeDex.js --- src/PokeDex.js | 115 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 16 deletions(-) diff --git a/src/PokeDex.js b/src/PokeDex.js index c7b071b04..af16de35e 100644 --- a/src/PokeDex.js +++ b/src/PokeDex.js @@ -1,13 +1,59 @@ -import "./App.css"; -import { useState, useEffect } from "react"; -import ReactLoading from "react-loading"; -import axios from "axios"; -import Modal from "react-modal"; +import './App.css'; +import { useState, useEffect } from 'react'; +import ReactLoading from 'react-loading'; +import axios from 'axios'; +import Modal from 'react-modal'; + +import DetailCard from './components/DetailCard'; +import ThumbnailCard from './components/ThumbnailCard'; +import { MdNavigateNext, MdNavigateBefore } from 'react-icons/md'; function PokeDex() { - const [pokemons, setPokemons] = useState([]); - const [pokemonDetail, setPokemonDetail] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [pokemons, setPokemons] = useState([]); + const [currentPokemonList, setCurrentPokemonList] = useState([]); + const [pokemonDetail, setPokemonDetail] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [nextLink, setNextLink] = useState(''); + const [prevLink, setPrevLink] = useState(''); + const [search, setSearch] = useState(''); + const [notfound, setNotfound] = useState(false); + const [pokemonApi, setPokemonApi] = useState('https://pokeapi.co/api/v2/pokemon'); + + useEffect(() => { + axios.get(pokemonApi).then((response) => { + console.log(response); + setPokemons(response.data.results); + setCurrentPokemonList(response.data.results); + setNextLink(response.data.next); + setPrevLink(response.data.previous); + }); + setIsLoading(false); + }, [pokemonApi]); + + useEffect(() => { + if (search === '') { + setPokemons(currentPokemonList); + setNotfound(false); + } else { + const searchedPokemon = currentPokemonList.filter((value) => { + return value.name.toLowerCase().includes(search.toLowerCase()); + }); + if (searchedPokemon.length > 0) { + setNotfound(false); + setPokemons(searchedPokemon); + } else { + setNotfound(true); + } + } + }, [search, currentPokemonList]); + + const handleChange = (e) => setSearch(e.target.value); + + + + const onClickNext = () => setPokemonApi(nextLink); + + const onClickPrev = () => setPokemonApi(prevLink); const customStyles = { content: { @@ -31,10 +77,13 @@ function PokeDex() {

Requirement:

@@ -56,15 +108,43 @@ function PokeDex() { <>
- Implement loader here +
) : ( <> -

Welcome to pokedex !

- Implement Pokedex list here - +

Welcome to pokedex !

+
+ + + +
+ {notfound &&

Couldn't find the searched pokemon!

} + {pokemons && ( +
+ {pokemons.map((pokemon, index) => ( + + ))} +
+ )} + )} {pokemonDetail && ( @@ -85,7 +165,10 @@ function PokeDex() { required in tabular format
  • Create a bar chart based on the stats above
  • -
  • Create a buttton to download the information generated in this modal as pdf. (images and chart must be included)
  • +
  • + Create a buttton to download the information generated in this + modal as pdf. (images and chart must be included) +
  • From 46c7923eaa624f3263f660af6ac430952ea8e07a Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Thu, 26 Jan 2023 22:32:43 +0800 Subject: [PATCH 02/13] Update Home.js --- src/Home.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Home.js b/src/Home.js index afa7452a0..f7305dd2f 100644 --- a/src/Home.js +++ b/src/Home.js @@ -1,4 +1,4 @@ -import "./App.css"; +import "../../App.css"; import { useState, useEffect } from "react"; import { NavLink } from "react-router-dom"; @@ -6,24 +6,40 @@ function Home() { const [text, setText] = useState(""); const [isReady, setIsReady] = useState(false); + const InputHandler = (event) => { + setText(event.target.value); + if (event.target.value === "Ready!") { + setIsReady(true); + } + if (event.target.value !== "Ready!"){ + setIsReady(false) + } + }; + return (
    - + + + Requirement: Try to show the hidden image and make it clickable that goes to /pokedex when the input below is "Ready!" remember to hide the red text away when "Ready!" is in the textbox.

    Are you ready to be a pokemon master?

    - - I am not ready yet! + + {isReady ? ( + "" + ) : ( + I am not ready yet! + )}
    ); From 846e1ca60f672950a91e416323a643331a57112c Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Thu, 26 Jan 2023 22:37:13 +0800 Subject: [PATCH 03/13] Add files via upload --- src/components/ThumbnailCard.css | 64 ++++++++++++++++++++++++++++++++ src/components/ThumbnailCard.jsx | 15 ++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/components/ThumbnailCard.css create mode 100644 src/components/ThumbnailCard.jsx diff --git a/src/components/ThumbnailCard.css b/src/components/ThumbnailCard.css new file mode 100644 index 000000000..146f1b0ae --- /dev/null +++ b/src/components/ThumbnailCard.css @@ -0,0 +1,64 @@ +.item-card { + width: 100px; + height: 135px; + margin-bottom: 20px; + text-align: center; + display: grid; + border: 1px white solid; + border-radius: 10px; + color: aliceblue; + cursor: pointer; +} +.item-card:hover { + background-color: yellow; + color: black; +} + +.img-box { + width: 100px; + height: 100px; + display: flex; + align-items: center; + justify-content: center; +} + +.item-img { + width: 80px; + height: 80px; +} +.item-img:hover { + width: 100px; + height: 100px; +} + +.item-name { + font-size: 16px; + padding-bottom: 8px; + font-weight: bold; + text-transform: capitalize; +} + +@media only screen and (min-width: 770px) { + .item-card { + width: 200px; + height: 250px; + } + + .img-box { + width: 200px; + height: 200px; + } + + .item-img { + width: 150px; + height: 150px; + } + .item-img:hover { + width: 200px; + height: 200px; + } + + .item-name { + font-size: 20px; + } + } \ No newline at end of file diff --git a/src/components/ThumbnailCard.jsx b/src/components/ThumbnailCard.jsx new file mode 100644 index 000000000..6da6b7f53 --- /dev/null +++ b/src/components/ThumbnailCard.jsx @@ -0,0 +1,15 @@ +import React from 'react' +import './ThumbnailCard.css' + +const ThumbnailCard = (props) => { + return ( +
    +
    + item thumbnail +
    + {props.name} +
    + ) +} + +export default ThumbnailCard \ No newline at end of file From bf5a5ad2f04e20c7d2a68ddd58e0b10450f81e3f Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Thu, 26 Jan 2023 22:42:11 +0800 Subject: [PATCH 04/13] Create DetailCard.css --- src/components/DetailCard.css | 84 +++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/components/DetailCard.css diff --git a/src/components/DetailCard.css b/src/components/DetailCard.css new file mode 100644 index 000000000..3afe87f24 --- /dev/null +++ b/src/components/DetailCard.css @@ -0,0 +1,84 @@ +.card-container { + width: 80vw; + height: auto; +} + +.pokemon-name { + font-size: xx-large; + font-weight: bold; + line-height: 0%; + text-transform: capitalize; +} + +.top-content { + display: grid; +} + +.pokemon-img { + width: 100%; + max-height: 200px; + object-fit: cover; +} + +table { + width: 100%; + text-align: left; + padding: 5px; + border: 1px aliceblue solid; +} + +td, th { + text-transform: capitalize; +} + +.table-wrapper { + display: flex; + align-items: center; +} + +.barchart-wrapper { + width: 100%; + padding-top: 20px; + margin: 0 auto; +} + +.bar-box { + background-color: #302e2e; + width: 150px; + height: 6px; +} + +.progress-bar { + background-color: yellow; + height: 6px; +} + +button { + padding: 5px; + background-color: yellow; + color: black; + margin-top: 20px; +} + +@media only screen and (min-width: 770px) { + .card-container { + width: 600px; + } + .top-content { + grid-template-columns: 50% 50%; + } + .pokemon-img { + width: 80%; + height: auto; + max-height: none; + object-fit: cover; + } + + .barchart-wrapper { + padding-top: 0; + } + + .bar-box { + width: 200px; + } +} From 96d2ddb4eb1cf918a2e796c7575dd4ca1c80aa3c Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Thu, 26 Jan 2023 22:42:41 +0800 Subject: [PATCH 05/13] Create DetailCard.jsx --- src/components/DetailCard.jsx | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/components/DetailCard.jsx diff --git a/src/components/DetailCard.jsx b/src/components/DetailCard.jsx new file mode 100644 index 000000000..9245cc3fd --- /dev/null +++ b/src/components/DetailCard.jsx @@ -0,0 +1,62 @@ +import React, { useRef } from "react"; +import './DetailCard.css'; +import { useReactToPrint } from "react-to-print"; + +const DetailCard = (props) => { + const printRef = useRef(null); // ref to point when print pdf is triggered + const name = props?.detail?.name; + const image = props?.detail?.sprites?.front_default; + const detailStats = props?.detail?.stats; + + const onDownloadPdf = useReactToPrint({ + content: () => printRef.current + }); + + const renderStatsTable = (stats) => ( +
    + + + + + + {stats.map((stat, index) => ( + + + + + ))} +
    NameBase Stats
    {stat.stat.name}{stat.base_stat}
    +
    + ); + + const renderBarChart = (stats) => ( +
    + + {stats.map((stat, index) => ( + + + + + ))} +
    {stat.stat.name} +
    +
    +
    +
    +
    + ); + + return ( +
    + {name} +
    + {name} + {renderStatsTable(detailStats)} +
    + {renderBarChart(detailStats)} + +
    + ); +}; + +export default DetailCard; From 96af31ad5748fddca1986a61276e87050c3eb605 Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Thu, 26 Jan 2023 22:49:00 +0800 Subject: [PATCH 06/13] Update Home.js --- src/Home.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Home.js b/src/Home.js index f7305dd2f..8a97f772e 100644 --- a/src/Home.js +++ b/src/Home.js @@ -46,3 +46,4 @@ function Home() { } export default Home; + From bffd93c867625945efde30d67bc6d80dcb25a79f Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Thu, 26 Jan 2023 22:49:51 +0800 Subject: [PATCH 07/13] Update PokeDex.js --- src/PokeDex.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/PokeDex.js b/src/PokeDex.js index af16de35e..4487f992d 100644 --- a/src/PokeDex.js +++ b/src/PokeDex.js @@ -49,7 +49,12 @@ function PokeDex() { const handleChange = (e) => setSearch(e.target.value); - + const onClickPokemon = (url) => { + axios.get(url).then((response) => { + console.log(response.data); + setPokemonDetail(response.data); + }); + }; const onClickNext = () => setPokemonApi(nextLink); @@ -140,7 +145,7 @@ function PokeDex() { {pokemons && (
    {pokemons.map((pokemon, index) => ( - + onClickPokemon(pokemon.url)} name={pokemon.name} /> ))}
    )} @@ -156,7 +161,8 @@ function PokeDex() { }} style={customStyles} > -
    + + {/*
    Requirement:
    • show the sprites front_default as the pokemon image
    • @@ -170,7 +176,7 @@ function PokeDex() { modal as pdf. (images and chart must be included)
    -
    +
    */} )} From 66b6b1ac4230773ad9b3e82c6c360325ec401f22 Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:51:59 +0800 Subject: [PATCH 08/13] Add files via upload --- src/App.css | 83 ++++- src/App.js | 40 ++- src/components/auth/AuhtForm.js | 149 +++++++++ src/components/auth/AuthForm.module.css | 187 +++++++++++ src/components/chat/ChatBox.css | 19 ++ src/components/chat/ChatBox.js | 39 +++ src/components/hooks/use-input.js | 49 +++ src/components/layout/DetailCard.css | 84 +++++ src/components/layout/DetailCard.jsx | 66 ++++ src/components/layout/Layout.js | 13 + src/components/layout/MainNavigation.js | 42 +++ .../layout/MainNavigation.module.css | 56 ++++ src/components/layout/ThumbnailCard.css | 65 ++++ src/components/layout/ThumbnailCard.jsx | 15 + src/components/pages/Home.js | 49 +++ src/components/pages/PokeDex.js | 296 ++++++++++++++++++ src/components/pages/ProfilePage.js | 6 + src/components/profile/ProfileForm.js | 50 +++ src/components/profile/ProfileForm.module.css | 45 +++ src/components/profile/UserProfile.js | 13 + src/components/profile/UserProfile.module.css | 9 + src/index.js | 24 +- src/store/auth-context.js | 92 ++++++ 23 files changed, 1469 insertions(+), 22 deletions(-) create mode 100644 src/components/auth/AuhtForm.js create mode 100644 src/components/auth/AuthForm.module.css create mode 100644 src/components/chat/ChatBox.css create mode 100644 src/components/chat/ChatBox.js create mode 100644 src/components/hooks/use-input.js create mode 100644 src/components/layout/DetailCard.css create mode 100644 src/components/layout/DetailCard.jsx create mode 100644 src/components/layout/Layout.js create mode 100644 src/components/layout/MainNavigation.js create mode 100644 src/components/layout/MainNavigation.module.css create mode 100644 src/components/layout/ThumbnailCard.css create mode 100644 src/components/layout/ThumbnailCard.jsx create mode 100644 src/components/pages/Home.js create mode 100644 src/components/pages/PokeDex.js create mode 100644 src/components/pages/ProfilePage.js create mode 100644 src/components/profile/ProfileForm.js create mode 100644 src/components/profile/ProfileForm.module.css create mode 100644 src/components/profile/UserProfile.js create mode 100644 src/components/profile/UserProfile.module.css create mode 100644 src/store/auth-context.js diff --git a/src/App.css b/src/App.css index 74b5e0534..a667c8ef9 100644 --- a/src/App.css +++ b/src/App.css @@ -1,7 +1,6 @@ .App { text-align: center; } - .App-logo { height: 40vmin; pointer-events: none; @@ -14,14 +13,14 @@ } .App-header { - background-color: #282c34; + background-image: linear-gradient(rgb(148, 148, 148), #a1e1ff, rgb(47, 46, 46)); min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); - color: white; + color: rgb(0, 0, 0); } .App-link { @@ -36,3 +35,81 @@ transform: rotate(360deg); } } + +.list-container { + display: grid; + width: 100vw; + grid-template-columns: auto auto auto; + align-items: center; + justify-items: center; +} + +.search-box { + width: 90vw; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.search-input { + width: 70%; + padding: 8px; +} + +.couldnt-find { + font-size: medium; + color: rgb(58, 52, 0); + margin-bottom: 30px; +} + +@media only screen and (min-width: 770px) { + .list-container { + max-width: 1280px; + } + + .search-box { + max-width: 1080px; + margin-bottom: 40px; + background-color:#1d1d10; + border: 2px solid white; + color: rgb(0, 61, 18); + font-weight: bold; + padding: 0.5rem 1.5rem; + border-radius: 50px; + } + + .search-input { + width: 40%; + padding: 8px; + } +} + +.react-icons { + color: white +} +.button { + font: inherit; + background-color: #a38c8c; + border: 1px solid white; + color: white; + font-weight: bold; + padding: 0.5rem 1.5rem; + border-radius: 6px; + cursor: pointer; +} +.h1-tittle { + background-color: #89f1f8; + border: 1px solid white; + color: white; + font-weight: bold; + padding: 0.5rem 1.5rem; + border-radius: 6px; +} + +.chat { + position: fixed; + + bottom: 0; + right: 0%; +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 710939b5a..cfc61a4c5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,17 +1,37 @@ import "./App.css"; -import Home from "./Home"; -import { Route, NavLink, HashRouter } from "react-router-dom"; -import PokeDex from "./PokeDex"; +import Home from "./components/pages/Home"; +import PokeDex from "./components/pages/PokeDex"; +import AuthForm from "./components/auth/AuhtForm"; +import Layout from "./components/layout/Layout"; +import AuthContext from "./store/auth-context"; +import { useContext } from "react"; +import { HashRouter, Route, Redirect } from "react-router-dom"; +import ProfilePage from "./components/pages/ProfilePage"; function App() { + const authCtx = useContext(AuthContext); return ( - -
    - - -
    - -
    + + +
    + + + + {authCtx.isLoggedIn && } + {!authCtx.isLoggedIn && } + + + {/* + {authCtx.isLoggedIn && } + {!authCtx.isLoggedIn && } + */} + + + + +
    +
    +
    ); } diff --git a/src/components/auth/AuhtForm.js b/src/components/auth/AuhtForm.js new file mode 100644 index 000000000..eeb2f373b --- /dev/null +++ b/src/components/auth/AuhtForm.js @@ -0,0 +1,149 @@ +import { useState, useRef, useContext } from 'react'; +import { useHistory } from 'react-router-dom'; +import useInput from '../hooks/use-input'; +import AuthContext from '../../store/auth-context'; +import classes from './AuthForm.module.css'; +const passwordisNotEmpty = (value) => value.trim() !== ''; +const isEmail = (value) => value.includes('@'); + +const AuthForm = () => { + const history = useHistory(); + const emailInputRef = useRef(); + const passwordInputRef = useRef(); + + const authCtx = useContext(AuthContext); + + const [isLogin, setIsLogin] = useState(true); + const [isLoading, setIsLoading] = useState(false); + + const { + value: emailValue, + hasError: emailHasError, + valueChangeHandler: emailChangeHandler, + inputBlurHandler: emailBlurHandler, + } = useInput(isEmail); + + const { + value: passwordValue, + hasError: passwordHasError, + valueChangeHandler: passwordChangeHandler, + inputBlurHandler: passwordBlurHandler, + + } = useInput(passwordisNotEmpty); + + + const switchAuthModeHandler = () => { + setIsLogin((prevState) => !prevState); + }; + + const submitHandler = (event) => { + event.preventDefault(); + + const enteredEmail = emailInputRef.current.value; + const enteredPassword = passwordInputRef.current.value; + + + // optional: Add validation + + setIsLoading(true); + let url; + if (isLogin) { + url = + 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSyC_3bBM-k5t2q3dNpQlcFFBonw1FB3UnSg'; + } else { + url = + 'https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSyC_3bBM-k5t2q3dNpQlcFFBonw1FB3UnSg'; + } + fetch(url, { + method: 'POST', + body: JSON.stringify({ + email: enteredEmail, + password: enteredPassword, + returnSecureToken: true, + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => { + setIsLoading(false); + if (res.ok) { + return res.json(); + } else { + return res.json().then((data) => { + let errorMessage = 'Authentication failed!'; + // if (data && data.error && data.error.message) { + // errorMessage = data.error.message; + // } + + throw new Error(errorMessage); + }); + } + }) + .then((data) => { + const expirationTime = new Date( + new Date().getTime() + +data.expiresIn * 1000 + ); + authCtx.login(data.idToken, expirationTime.toISOString()); + history.replace('/pokedex'); + }) + .catch((err) => { + alert(err.message); + }); + }; + + return ( +
    +

    {isLogin ? 'Login' : 'Sign Up'}

    +
    +
    + + + + + + + + {emailHasError &&

    Email address should have "@"

    } + +
    +
    + + + {passwordHasError &&

    Please enter a password.

    } +
    +
    + {!isLoading && ( + + )} + {isLoading &&

    Sending request...

    } + +
    +
    +
    + + ); +}; + +export default AuthForm; diff --git a/src/components/auth/AuthForm.module.css b/src/components/auth/AuthForm.module.css new file mode 100644 index 000000000..f2fad6808 --- /dev/null +++ b/src/components/auth/AuthForm.module.css @@ -0,0 +1,187 @@ + + +.auth { + margin: 3rem auto; + width: 95%; + max-width: 25rem; + border-radius: 6px; + background-color: #3bca9a; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + padding: 1rem; + text-align: center; + color: #282c34; +} + +.auth h1 { + text-align: center; + color: white; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + display: block; + color: white; + font-weight: bold; + margin-bottom: 0.5rem; +} + +.control input { + font: inherit; + background-color: #f1e1fc; + color: #000000; + border-radius: 4px; + border: 1px solid white; + width: 100%; + text-align: left; + padding: 0.25rem; +} + +.actions { + margin-top: 1.5rem; + display: flex; + flex-direction: column; + align-items: center; +} + +.actions button { + cursor: pointer; + font: inherit; + color: white; + background-color: #6ee5ee; + border: 1px solid #6ee5ee; + border-radius: 4px; + padding: 0.5rem 2.5rem; +} + +.actions button:hover { + background-color: #f8fc22; + border-color: #f8fc22; +} + +.actions .toggle { + margin-top: 1rem; + background-color: transparent; + color: #f8fc22; + border: none; + padding: 0.15rem 1.5rem; +} + +.actions .toggle:hover { + background-color: transparent; + color: #f8fc22; +} + +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #3f3f3f; +} + +.app { + width: 90%; + max-width: 43rem; + padding: 1rem; + border-radius: 12px; + background-color: rgb(202, 245, 239); + margin: 3rem auto; +} + +.form-control { + margin-bottom: 1rem; +} + +.form-control input, +.form-control label { + display: block; +} + +.form-control label { + font-weight: bold; + margin-bottom: 0.5rem; +} + +.form-control input, +.form-control select { + font: inherit; + padding: 0.5rem; + border-radius: 4px; + border: 1px solid #ccc; + width: 20rem; + max-width: 100%; +} + +.form-control input:focus { + outline: none; + border-color: #47f5bb; + background-color: #050505; +} + +.control-group { + display: flex; + column-gap: 1rem; + flex-wrap: wrap; +} + +.control-group .form-control { + min-width: 15rem; + flex: 1; +} + +button { + font: inherit; + background-color: #03b4ab; + color: white; + border: 1px solid #010002; + padding: 0.5rem 1.5rem; + border-radius: 6px; + cursor: pointer; +} + +button:hover, +button:active { + background-color: #059e9e; + border-color: #059e9e; +} + +button:disabled, +button:disabled:hover, +button:disabled:active { + background-color: #ccc; + color: #292929; + border-color: #ccc; + cursor: not-allowed; +} + +.form-actions { + text-align: right; +} + +.form-actions button { + margin-left: 1rem; +} + +.invalid input { + border: 1px solid #b40e0e; + background-color: #fddddd; +} + +.invalid input:focus { + border-color: #ff8800; + background-color: #fbe8d2; +} + +.errortext { + color: #ed1111; +} \ No newline at end of file diff --git a/src/components/chat/ChatBox.css b/src/components/chat/ChatBox.css new file mode 100644 index 000000000..dbe918de2 --- /dev/null +++ b/src/components/chat/ChatBox.css @@ -0,0 +1,19 @@ +.chat-popup { + position: fixed; + bottom: 0; + right: 0; + } + + .chat { + display: none; + } + +.conversation { + font-size: 10px; +} + + .Hidden { + position: fixed; + bottom: 0; + right: 0; + } \ No newline at end of file diff --git a/src/components/chat/ChatBox.js b/src/components/chat/ChatBox.js new file mode 100644 index 000000000..b3921c1ef --- /dev/null +++ b/src/components/chat/ChatBox.js @@ -0,0 +1,39 @@ +import React, { useState } from 'react'; + +const ChatBox = ({ openButtonLabel }) => { + const [conversation, setConversation] = useState([]); + const [message, setMessage] = useState(''); + const [isOpen, setIsOpen] = useState(false); + + const handleChange = event => { + setMessage(event.target.value); + }; + + const handleSubmit = event => { + event.preventDefault(); + setConversation(prevConversation => [...prevConversation, message]); + setMessage(''); + }; + + const renderConversation = () => { + return conversation.map((message, index) => ( +
  • {message}
  • + )); + }; + + return ( +
    + + {isOpen ?
    + +
    + + +
    +
    + : ""} +
    + ); +}; + +export default ChatBox; diff --git a/src/components/hooks/use-input.js b/src/components/hooks/use-input.js new file mode 100644 index 000000000..b01384598 --- /dev/null +++ b/src/components/hooks/use-input.js @@ -0,0 +1,49 @@ +import { useReducer } from 'react'; + +const initialInputState = { + value: '', + isTouched: false, +}; + +const inputStateReducer = (state, action) => { + if (action.type === 'INPUT') { + return { value: action.value, isTouched: state.isTouched }; + } + if (action.type === 'BLUR') { + return { isTouched: true, value: state.value }; + } + + + return inputStateReducer; +}; + +const useInput = (validateValue) => { + const [inputState, dispatch] = useReducer( + inputStateReducer, + initialInputState + ); + + const valueIsValid = validateValue(inputState.value); + const hasError = !valueIsValid && inputState.isTouched; + + const valueChangeHandler = (event) => { + dispatch({ type: 'INPUT', value: event.target.value }); + }; + + const inputBlurHandler = (event) => { + dispatch({ type: 'BLUR' }); + }; + + + + return { + value: inputState.value, + isValid: valueIsValid, + hasError, + valueChangeHandler, + inputBlurHandler, + + }; +}; + +export default useInput; diff --git a/src/components/layout/DetailCard.css b/src/components/layout/DetailCard.css new file mode 100644 index 000000000..dac0cc2b5 --- /dev/null +++ b/src/components/layout/DetailCard.css @@ -0,0 +1,84 @@ +.card-container { + width: 80vw; + height: auto; +} + +.pokemon-name { + font-size: xx-large; + font-weight: bold; + line-height: 0%; + text-transform: capitalize; +} + +.top-content { + display: grid; +} + +.pokemon-img { + width: 100%; + max-height: 100%; + object-fit: cover; +} + +table { + width: 100%; + text-align: left; + padding: 5px; + border: 1px aliceblue solid; +} + +td, th { + text-transform: capitalize; +} + +.table-wrapper { + display: flex; + align-items: center; +} + +.barchart-wrapper { + width: 100%; + padding-top: 20px; + margin: 0 auto; +} + +.bar-box { + background-color: #302e2e; + width: 150px; + height: 6px; +} + +.progress-bar { + background-color: yellow; + height: 6px; +} + +button { + padding: 5px; + background-color: yellow; + color: black; + margin-top: 20px; +} + +@media only screen and (min-width: 770px) { + .card-container { + width: 600px; + } + .top-content { + grid-template-columns: 50% 50%; + } + .pokemon-img { + width: 80%; + height: auto; + max-height: none; + object-fit: cover; + } + + .barchart-wrapper { + padding-top: 0; + } + + .bar-box { + width: 200px; + } +} \ No newline at end of file diff --git a/src/components/layout/DetailCard.jsx b/src/components/layout/DetailCard.jsx new file mode 100644 index 000000000..685e54f23 --- /dev/null +++ b/src/components/layout/DetailCard.jsx @@ -0,0 +1,66 @@ +import React, { useRef } from "react"; +import './DetailCard.css'; +import { useReactToPrint } from "react-to-print"; + +const DetailCard = (props) => { + const printRef = useRef(null); // ref to point when print pdf is triggered + const name = props?.detail?.name; + const image = props?.detail?.sprites?.front_default; + const detailStats = props?.detail?.stats; + + const onDownloadPdf = useReactToPrint({ + content: () => printRef.current + }); + + const renderStatsTable = (stats) => ( +
    + + + + + + + {stats.map((stat, index) => ( + + + + + ))} + +
    NameBase Stats
    {stat.stat.name}{stat.base_stat}
    +
    + ); + + const renderBarChart = (stats) => ( +
    + + + {stats.map((stat, index) => ( + + + + + ))} + +
    {stat.stat.name} +
    +
    +
    +
    +
    + ); + + return ( +
    + {name} +
    + {name} + {renderStatsTable(detailStats)} +
    + {renderBarChart(detailStats)} + +
    + ); +}; + +export default DetailCard; diff --git a/src/components/layout/Layout.js b/src/components/layout/Layout.js new file mode 100644 index 000000000..95aaf1646 --- /dev/null +++ b/src/components/layout/Layout.js @@ -0,0 +1,13 @@ +import { Fragment } from "react"; + +import MainNavigation from "./MainNavigation"; +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/src/components/layout/MainNavigation.js b/src/components/layout/MainNavigation.js new file mode 100644 index 000000000..e33710d01 --- /dev/null +++ b/src/components/layout/MainNavigation.js @@ -0,0 +1,42 @@ +import { useContext } from "react"; +import { NavLink } from "react-router-dom"; + +import AuthContext from "../../store/auth-context"; +import classes from "./MainNavigation.module.css"; + +const MainNavigation = () => { + const authCtx = useContext(AuthContext); + + const isLoggedIn = authCtx.isLoggedIn; + + const logoutHandler = () => { + authCtx.logout(); + // optional: redirect the user + }; + + return ( +
    + +
    PokeDex
    +
    + +
    + ); +}; + +export default MainNavigation; diff --git a/src/components/layout/MainNavigation.module.css b/src/components/layout/MainNavigation.module.css new file mode 100644 index 000000000..0ddc83dd4 --- /dev/null +++ b/src/components/layout/MainNavigation.module.css @@ -0,0 +1,56 @@ +.header { + box-sizing: border-box; + width: 100%; + height: 5rem; + background-color: #00858a; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 10%; + border: 2px solid white; +} + +.logo { + font-family: 'Lato', sans-serif; + font-size: 2rem; + color: white; + margin: 0; +} + +.header ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + align-items: baseline; +} + +.header li { + margin: 0 1rem; + font: inherit; + background-color: transparent; + border: 1px solid white; + color: white; + font-weight: bold; + padding: 0.5rem 1.5rem; + border-radius: 6px; + cursor: pointer; +} + +.header a { + text-decoration: none; + color: white; + font-weight: bold; +} + + + +.header a:hover { + color: #c291e2; +} + +.header button:hover { + background-color: #c291e2; + color: #38015c; +} \ No newline at end of file diff --git a/src/components/layout/ThumbnailCard.css b/src/components/layout/ThumbnailCard.css new file mode 100644 index 000000000..4b13f9313 --- /dev/null +++ b/src/components/layout/ThumbnailCard.css @@ -0,0 +1,65 @@ +.item-card { + width: 100px; + height: 135px; + margin-bottom: 20px; + text-align: center; + display: grid; + border: 1px white solid; + border-radius: 10px; + color: aliceblue; + cursor: pointer; + background-color: #566574; +} +.item-card:hover { + background-color: rgb(112, 244, 253); + color: black; +} + +.img-box { + width: 100px; + height: 100px; + display: flex; + align-items: center; + justify-content: center; +} + +.item-img { + width: 80px; + height: 80px; +} +.item-img:hover { + width: 100px; + height: 100px; +} + +.item-name { + font-size: 16px; + padding-bottom: 8px; + font-weight: bold; + text-transform: capitalize; +} + +@media only screen and (min-width: 770px) { + .item-card { + width: 200px; + height: 250px; + } + + .img-box { + width: 200px; + height: 200px; + } + + .item-img { + width: 150px; + height: 150px; + } + .item-img:hover { + width: 200px; + height: 200px; + } + + .item-name { + font-size: 20px; + } + } \ No newline at end of file diff --git a/src/components/layout/ThumbnailCard.jsx b/src/components/layout/ThumbnailCard.jsx new file mode 100644 index 000000000..6da6b7f53 --- /dev/null +++ b/src/components/layout/ThumbnailCard.jsx @@ -0,0 +1,15 @@ +import React from 'react' +import './ThumbnailCard.css' + +const ThumbnailCard = (props) => { + return ( +
    +
    + item thumbnail +
    + {props.name} +
    + ) +} + +export default ThumbnailCard \ No newline at end of file diff --git a/src/components/pages/Home.js b/src/components/pages/Home.js new file mode 100644 index 000000000..0de052811 --- /dev/null +++ b/src/components/pages/Home.js @@ -0,0 +1,49 @@ +import "../../App.css"; +import { useState } from "react"; +import { NavLink } from "react-router-dom"; + +function Home() { + const [text, setText] = useState(""); + const [isReady, setIsReady] = useState(false); + + const InputHandler = (event) => { + setText(event.target.value); + if (event.target.value === "Ready!") { + setIsReady(true); + } + if (event.target.value !== "Ready!"){ + setIsReady(false) + } + }; + + + return ( +
    +
    + + + + + Requirement: Try to show the hidden image and make it clickable that + goes to /pokedex when the input below is "Ready!" remember to hide the + red text away when "Ready!" is in the textbox. + +

    Are you ready to be a pokemon master?

    + + {isReady ? ( + "" + ) : ( + I am not ready yet! + )} +
    +
    + ); +} + +export default Home; diff --git a/src/components/pages/PokeDex.js b/src/components/pages/PokeDex.js new file mode 100644 index 000000000..ba9ad6cd2 --- /dev/null +++ b/src/components/pages/PokeDex.js @@ -0,0 +1,296 @@ +import "../../App.css"; +import { useState, useEffect } from "react"; +import ReactLoading from "react-loading"; +import axios from "axios"; +import Modal from "react-modal"; +import ChatBox from "../chat/ChatBox"; +import DetailCard from "../layout/DetailCard"; +import ThumbnailCard from "../layout/ThumbnailCard"; +import { MdNavigateNext, MdNavigateBefore } from "react-icons/md"; + +function PokeDex() { + const [pokemons, setPokemons] = useState([]); + const [currentPokemonList, setCurrentPokemonList] = useState([]); + // const [originalPokemons, setOriginalPokemons] = useState([]); + const [pokemonDetail, setPokemonDetail] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isOpen,setIsOpen] = useState (false); + const [nextLink, setNextLink] = useState(""); + const [prevLink, setPrevLink] = useState(""); + const [search, setSearch] = useState(""); + const [notfound, setNotfound] = useState(false); + const [sortAscending, setSortAscending] = useState(true); + const [pokemonApi, setPokemonApi] = useState( + "https://pokeapi.co/api/v2/pokemon" + + ); + const [currentPage, setCurrentPage] = useState(1); + + + const [searchApiFetch, setSearchApiFetch] = useState([]); + + const searchApi = "https://pokeapi.co/api/v2/pokemon?offset=0&limit=5000"; + + useEffect(() => { + axios.get(pokemonApi, { + timeout: 5000 + }).then((response) => { + console.log(response); + setPokemons(response.data.results); + setCurrentPokemonList(response.data.results); + setNextLink(response.data.next); + setPrevLink(response.data.previous); + localStorage.setItem('OriginalPokemon',JSON.stringify(response.data.results)); + setIsLoading(false); + }).catch(error => { + if (error.code === 'ECONNABORTED') { + console.log('Request timed out'); + setIsLoading(true) + } else { + console.log('Other error:', error); + setIsLoading(true) + } + }); + + }, [pokemonApi]); + + + + + useEffect(() => { + axios.get(searchApi).then((response) => { + setSearchApiFetch(response.data.results); + }); + if (search === "") { + setPokemons(currentPokemonList); + setNotfound(false); + } else { + const searchedPokemon = searchApiFetch.filter((value) => { + return value.name.toLowerCase().includes(search.toLowerCase()); + }); + if (searchedPokemon.length > 0) { + setNotfound(false); + setPokemons(searchedPokemon); + } else { + setNotfound(true); + } + } + }, [search, searchApiFetch, currentPokemonList, pokemons]); // add pokemons to the dependency array + + + + const handleChange = (e) => setSearch(e.target.value); + + + const handleSortToggle = () => { + setSortAscending(!sortAscending); + const sortedPokemons = pokemons.sort((a, b) => { + if (sortAscending) { + return a.name > b.name ? 1 : -1; + } else { + return a.name < b.name ? 1 : -1; + } + }); + setPokemons(sortedPokemons); + }; + + + + const resetButton = () => { + const pokereset = JSON.parse(localStorage.getItem('OriginalPokemon')); + setCurrentPokemonList(pokereset) + setPokemons(pokereset); // reset pokemons to originalPokemons + console.log(pokereset); // add this line + }; + + + const refresh = () => { + setIsLoading(true); + axios.get(pokemonApi, { + timeout: 5000 + }).then((response) => { + console.log(response); + setPokemons(response.data.results); + setCurrentPokemonList(response.data.results); + setNextLink(response.data.next); + setPrevLink(response.data.previous); + localStorage.setItem('OriginalPokemon',JSON.stringify(response.data.results)); + setIsLoading(false); + }).catch(error => { + if (error.code === 'ECONNABORTED') { + console.log('Request timed out'); + setIsLoading(true) + } else { + console.log('Other error:', error); + setIsLoading(true) + } + }); + }; + + + + + const onClickPokemon = (url) => { + axios.get(url).then((response) => { + console.log(response.data); + setPokemonDetail(response.data); + setIsOpen(true) + }); + }; + + + + const onClickNext = () => { + setCurrentPage(currentPage + 1) + setPokemonApi(nextLink) + console.log(currentPage)}; + + const onClickPrev = () => { + setCurrentPage(currentPage - 1) + setPokemonApi(prevLink) + console.log(currentPage)}; + + const customStyles = { + content: { + top: "50%", + left: "50%", + right: "auto", + bottom: "auto", + marginRight: "-50%", + transform: "translate(-50%, -50%)", + background: "black", + color: "white", + }, + overlay: { backgroundColor: "grey" }, + }; + + if (!isLoading && pokemons.length === 0) { + return ( +
    +
    +

    Welcome to pokedex !

    +

    Requirement:

    + +
    +
    + ); + } + + return ( +
    +
    + {isLoading ? ( + <> +
    +
    + + +
    +
    + + ) : ( + <> + +

    Welcome to pokedex !

    +

    PAGE:{currentPage}

    +
    + + + + + +
    + + + {notfound && ( +

    + Couldn't find the pokemon! +

    + )} + {pokemons && ( +
    + {pokemons.map((pokemon, index) => ( + onClickPokemon(pokemon.url)} + name={pokemon.name} + /> + ))} +
    + )} + + )} +
    + +
    + +
    + {pokemonDetail && ( + { + setPokemonDetail(null); + setIsOpen(false) + + }} + style={customStyles} + > + + {/*
    + Requirement: +
      +
    • show the sprites front_default as the pokemon image
    • +
    • Show the stats details - only stat.name and base_stat is required in tabular format
    • +
    • Create a bar chart based on the stats above
    • +
    • + Create a buttton to download the information generated in this modal as pdf. (images and chart must be + included) +
    • +
    +
    */} +
    + )} + +
    + ); +} + +export default PokeDex; diff --git a/src/components/pages/ProfilePage.js b/src/components/pages/ProfilePage.js new file mode 100644 index 000000000..efd95e85c --- /dev/null +++ b/src/components/pages/ProfilePage.js @@ -0,0 +1,6 @@ +import UserProfile from "../profile/UserProfile"; +const ProfilePage = () => { + return ; +}; + +export default ProfilePage; diff --git a/src/components/profile/ProfileForm.js b/src/components/profile/ProfileForm.js new file mode 100644 index 000000000..72b85704a --- /dev/null +++ b/src/components/profile/ProfileForm.js @@ -0,0 +1,50 @@ +import { useRef, useContext } from 'react'; +import { useHistory } from 'react-router-dom'; + +import AuthContext from '../../store/auth-context'; +import classes from './ProfileForm.module.css'; + +const ProfileForm = () => { + const history = useHistory(); + + const newPasswordInputRef = useRef(); + const authCtx = useContext(AuthContext); + + const submitHandler = (event) => { + event.preventDefault(); + + const enteredNewPassword = newPasswordInputRef.current.value; + + // add validation + + fetch('https://identitytoolkit.googleapis.com/v1/accounts:update?AIzaSyC_3bBM-k5t2q3dNpQlcFFBonw1FB3UnSg', { + method: 'POST', + body: JSON.stringify({ + idToken: authCtx.token, + password: enteredNewPassword, + returnSecureToken: false + }), + headers: { + 'Content-Type': 'application/json' + } + }).then(res => { + // assumption: Always succeeds! + + history.replace('/'); + }); + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default ProfileForm; diff --git a/src/components/profile/ProfileForm.module.css b/src/components/profile/ProfileForm.module.css new file mode 100644 index 000000000..4b509df65 --- /dev/null +++ b/src/components/profile/ProfileForm.module.css @@ -0,0 +1,45 @@ +.form { + width: 95%; + max-width: 25rem; + margin: 2rem auto; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + margin-bottom: 0.5rem; + color: aliceblue ; + display: block; +} + +.control input { + display: block; + font: inherit; + width: 100%; + border-radius: 4px; + border: 1px solid #38015c; + padding: 0.25rem; + background-color: #f7f0fa; +} + +.action { + margin-top: 1.5rem; +} + +.action button { + font: inherit; + cursor: pointer; + padding: 0.5rem 1.5rem; + border-radius: 4px; + background-color: #38015c; + color: white; + border: 1px solid #38015c; +} + +.action button:hover { + background-color: #540d83; + border-color: #540d83; +} \ No newline at end of file diff --git a/src/components/profile/UserProfile.js b/src/components/profile/UserProfile.js new file mode 100644 index 000000000..bf6b4d84a --- /dev/null +++ b/src/components/profile/UserProfile.js @@ -0,0 +1,13 @@ +import ProfileForm from './ProfileForm'; +import classes from './UserProfile.module.css'; + +const UserProfile = () => { + return ( +
    +

    Your User Profile

    + +
    + ); +}; + +export default UserProfile; diff --git a/src/components/profile/UserProfile.module.css b/src/components/profile/UserProfile.module.css new file mode 100644 index 000000000..6218d280e --- /dev/null +++ b/src/components/profile/UserProfile.module.css @@ -0,0 +1,9 @@ +.profile { + margin: 3rem auto; + text-align: center; +} + +.profile h1 { + font-size: 5rem; + color: aliceblue; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index ef2edf8ea..2cfebee86 100644 --- a/src/index.js +++ b/src/index.js @@ -1,14 +1,20 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import React from "react"; +import ReactDOM from "react-dom"; +import { BrowserRouter } from 'react-router-dom'; +import { AuthContextProvider } from './store/auth-context'; +import "./index.css"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; ReactDOM.render( - - - , - document.getElementById('root') + + + + + + + , + document.getElementById("root") ); // If you want to start measuring performance in your app, pass a function diff --git a/src/store/auth-context.js b/src/store/auth-context.js new file mode 100644 index 000000000..66fa0f4ef --- /dev/null +++ b/src/store/auth-context.js @@ -0,0 +1,92 @@ +import React, { useState, useEffect, useCallback } from 'react'; + +let logoutTimer; + +const AuthContext = React.createContext({ + token: '', + isLoggedIn: false, + login: (token) => {}, + logout: () => {}, +}); + +const calculateRemainingTime = (expirationTime) => { + const currentTime = new Date().getTime(); + const adjExpirationTime = new Date(expirationTime).getTime(); + + const remainingDuration = adjExpirationTime - currentTime; + + return remainingDuration; +}; + +const retrieveStoredToken = () => { + const storedToken = localStorage.getItem('token'); + const storedExpirationDate = localStorage.getItem('expirationTime'); + + const remainingTime = calculateRemainingTime(storedExpirationDate); + + if (remainingTime <= 10000) { + localStorage.removeItem('token'); + localStorage.removeItem('expirationTime'); + return null; + } + + return { + token: storedToken, + duration: remainingTime, + }; +}; + +export const AuthContextProvider = (props) => { + const tokenData = retrieveStoredToken(); + + let initialToken; + if (tokenData) { + initialToken = tokenData.token; + } + + const [token, setToken] = useState(initialToken); + + const userIsLoggedIn = !!token; + + const logoutHandler = useCallback(() => { + setToken(null); + localStorage.removeItem('token'); + localStorage.removeItem('expirationTime'); + + if (logoutTimer) { + clearTimeout(logoutTimer); + } + }, []); + + const loginHandler = (token, expirationTime) => { + setToken(token); + localStorage.setItem('token', token); + localStorage.setItem('expirationTime', expirationTime); + + const remainingTime = calculateRemainingTime(expirationTime); + + logoutTimer = setTimeout(logoutHandler, remainingTime); + }; + + useEffect(() => { + if (tokenData) { + console.log(tokenData.duration); + logoutTimer = setTimeout(logoutHandler, tokenData.duration); + } + }, [tokenData, logoutHandler]); + + const contextValue = { + token: token, + isLoggedIn: userIsLoggedIn, + login: loginHandler, + logout: logoutHandler, + }; + + return ( + + {props.children} + + ); +}; + +export default AuthContext; From 1a47f7037bf794583295ba91eb4528d1aad8d633 Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:55:05 +0800 Subject: [PATCH 09/13] Delete PokeDex.js --- src/PokeDex.js | 186 ------------------------------------------------- 1 file changed, 186 deletions(-) delete mode 100644 src/PokeDex.js diff --git a/src/PokeDex.js b/src/PokeDex.js deleted file mode 100644 index 4487f992d..000000000 --- a/src/PokeDex.js +++ /dev/null @@ -1,186 +0,0 @@ -import './App.css'; -import { useState, useEffect } from 'react'; -import ReactLoading from 'react-loading'; -import axios from 'axios'; -import Modal from 'react-modal'; - -import DetailCard from './components/DetailCard'; -import ThumbnailCard from './components/ThumbnailCard'; -import { MdNavigateNext, MdNavigateBefore } from 'react-icons/md'; - -function PokeDex() { - const [pokemons, setPokemons] = useState([]); - const [currentPokemonList, setCurrentPokemonList] = useState([]); - const [pokemonDetail, setPokemonDetail] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [nextLink, setNextLink] = useState(''); - const [prevLink, setPrevLink] = useState(''); - const [search, setSearch] = useState(''); - const [notfound, setNotfound] = useState(false); - const [pokemonApi, setPokemonApi] = useState('https://pokeapi.co/api/v2/pokemon'); - - useEffect(() => { - axios.get(pokemonApi).then((response) => { - console.log(response); - setPokemons(response.data.results); - setCurrentPokemonList(response.data.results); - setNextLink(response.data.next); - setPrevLink(response.data.previous); - }); - setIsLoading(false); - }, [pokemonApi]); - - useEffect(() => { - if (search === '') { - setPokemons(currentPokemonList); - setNotfound(false); - } else { - const searchedPokemon = currentPokemonList.filter((value) => { - return value.name.toLowerCase().includes(search.toLowerCase()); - }); - if (searchedPokemon.length > 0) { - setNotfound(false); - setPokemons(searchedPokemon); - } else { - setNotfound(true); - } - } - }, [search, currentPokemonList]); - - const handleChange = (e) => setSearch(e.target.value); - - const onClickPokemon = (url) => { - axios.get(url).then((response) => { - console.log(response.data); - setPokemonDetail(response.data); - }); - }; - - const onClickNext = () => setPokemonApi(nextLink); - - const onClickPrev = () => setPokemonApi(prevLink); - - const customStyles = { - content: { - top: "50%", - left: "50%", - right: "auto", - bottom: "auto", - marginRight: "-50%", - transform: "translate(-50%, -50%)", - background: "black", - color: "white", - }, - overlay: { backgroundColor: "grey" }, - }; - - if (!isLoading && pokemons.length === 0) { - return ( -
    -
    -

    Welcome to pokedex !

    -

    Requirement:

    -
      -
    • - Call this api:https://pokeapi.co/api/v2/pokemon to get pokedex, - and show a list of pokemon name. -
    • -
    • Implement React Loading and show it during API call
    • -
    • - when hover on the list item , change the item color to yellow. -
    • -
    • when clicked the list item, show the modal below
    • -
    • - Add a search bar on top of the bar for searching, search will run - on keyup event -
    • -
    • Implement sorting and pagingation
    • -
    • Commit your codes after done
    • -
    • - If you do more than expected (E.g redesign the page / create a - chat feature at the bottom right). it would be good. -
    • -
    -
    -
    - ); - } - - return ( -
    -
    - {isLoading ? ( - <> -
    -
    - -
    -
    - - ) : ( - <> -

    Welcome to pokedex !

    -
    - - - -
    - {notfound &&

    Couldn't find the searched pokemon!

    } - {pokemons && ( -
    - {pokemons.map((pokemon, index) => ( - onClickPokemon(pokemon.url)} name={pokemon.name} /> - ))} -
    - )} - - )} -
    - {pokemonDetail && ( - { - setPokemonDetail(null); - }} - style={customStyles} - > - - {/*
    - Requirement: -
      -
    • show the sprites front_default as the pokemon image
    • -
    • - Show the stats details - only stat.name and base_stat is - required in tabular format -
    • -
    • Create a bar chart based on the stats above
    • -
    • - Create a buttton to download the information generated in this - modal as pdf. (images and chart must be included) -
    • -
    -
    */} -
    - )} -
    - ); -} - -export default PokeDex; From 5083db00cfcc13d63a19bc8880e011e20aeefda7 Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:55:14 +0800 Subject: [PATCH 10/13] Delete Home.js --- src/Home.js | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 src/Home.js diff --git a/src/Home.js b/src/Home.js deleted file mode 100644 index 8a97f772e..000000000 --- a/src/Home.js +++ /dev/null @@ -1,49 +0,0 @@ -import "../../App.css"; -import { useState, useEffect } from "react"; -import { NavLink } from "react-router-dom"; - -function Home() { - const [text, setText] = useState(""); - const [isReady, setIsReady] = useState(false); - - const InputHandler = (event) => { - setText(event.target.value); - if (event.target.value === "Ready!") { - setIsReady(true); - } - if (event.target.value !== "Ready!"){ - setIsReady(false) - } - }; - - return ( -
    -
    - - - - - Requirement: Try to show the hidden image and make it clickable that - goes to /pokedex when the input below is "Ready!" remember to hide the - red text away when "Ready!" is in the textbox. - -

    Are you ready to be a pokemon master?

    - - {isReady ? ( - "" - ) : ( - I am not ready yet! - )} -
    -
    - ); -} - -export default Home; - From b64dbaa51df9c5f991dee7f0ef67c2eb803c3197 Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:58:21 +0800 Subject: [PATCH 11/13] Update PokeDex.js --- src/components/pages/PokeDex.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/pages/PokeDex.js b/src/components/pages/PokeDex.js index ba9ad6cd2..0164d3f95 100644 --- a/src/components/pages/PokeDex.js +++ b/src/components/pages/PokeDex.js @@ -24,8 +24,6 @@ function PokeDex() { "https://pokeapi.co/api/v2/pokemon" ); - const [currentPage, setCurrentPage] = useState(1); - const [searchApiFetch, setSearchApiFetch] = useState([]); @@ -140,15 +138,9 @@ function PokeDex() { - const onClickNext = () => { - setCurrentPage(currentPage + 1) - setPokemonApi(nextLink) - console.log(currentPage)}; + const onClickNext = () => setPokemonApi(nextLink); - const onClickPrev = () => { - setCurrentPage(currentPage - 1) - setPokemonApi(prevLink) - console.log(currentPage)}; + const onClickPrev = () => setPokemonApi(prevLink); const customStyles = { content: { From 6345296e566076a4af57437f23d4246ed4ca4c83 Mon Sep 17 00:00:00 2001 From: shafiqns <115532625+shafiqns@users.noreply.github.com> Date: Fri, 27 Jan 2023 13:01:22 +0800 Subject: [PATCH 12/13] Add files via upload --- public/pokeball.png | Bin 0 -> 383770 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/pokeball.png diff --git a/public/pokeball.png b/public/pokeball.png new file mode 100644 index 0000000000000000000000000000000000000000..c947a3126d6b7af2d6919d28d820d97d818e881d GIT binary patch literal 383770 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

    Yb!oI?xiS+|z1hfk@DkW8GDQSV~l(yJyQurj1k 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