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:
- Call this api:https://pokeapi.co/api/v2/pokemon to get pokedex, and show a list of pokemon name.
+ 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 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
@@ -42,7 +91,10 @@ function PokeDex() {
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.
+
+ If you do more than expected (E.g redesign the page / create a
+ chat feature at the bottom right). it would be good.
+
@@ -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 (
);
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 (
+
+
+
+
+
{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) => (
+
+
+
+ Name
+ Base Stats
+
+ {stats.map((stat, index) => (
+
+ {stat.stat.name}
+ {stat.base_stat}
+
+ ))}
+
+
+ );
+
+ const renderBarChart = (stats) => (
+
+
+ {stats.map((stat, index) => (
+
+ {stat.stat.name}
+
+
+
+
+ ))}
+
+
+ );
+
+ return (
+
+
{name}
+
+
+ {renderStatsTable(detailStats)}
+
+ {renderBarChart(detailStats)}
+
Download As PDF
+
+ );
+};
+
+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'}
+
+
+
+ );
+};
+
+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 (
+
+
setIsOpen(!isOpen)}>{openButtonLabel}
+ {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) => (
+
+
+
+
+ Name
+ Base Stats
+
+ {stats.map((stat, index) => (
+
+ {stat.stat.name}
+ {stat.base_stat}
+
+ ))}
+
+
+
+ );
+
+ const renderBarChart = (stats) => (
+
+
+
+ {stats.map((stat, index) => (
+
+ {stat.stat.name}
+
+
+
+
+ ))}
+
+
+
+ );
+
+ return (
+
+
{name}
+
+
+ {renderStatsTable(detailStats)}
+
+ {renderBarChart(detailStats)}
+
Download As PDF
+
+ );
+};
+
+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
+
+
+
+ {!isLoggedIn && (
+
+ Login
+
+ )}
+ {isLoggedIn && (
+
+ PokeDex
+
+ )}
+
+ {isLoggedIn && Logout }
+
+
+
+ );
+};
+
+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 (
+
+
+
+
+
{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 (
+
+ );
+}
+
+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:
+
+
+ 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 !
+ PAGE:{currentPage}
+
+
+ Sort A-Z/Z-A
+
+ Reset
+
+
+
+
+ {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 (
+
+ );
+};
+
+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 (
-
- );
-}
-
-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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