diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx index 565f017..fa20f08 100644 --- a/src/components/app/App.tsx +++ b/src/components/app/App.tsx @@ -1,9 +1,9 @@ -import React from "react"; -import Home from "@components/home/Home"; -import Header from "@components/header/Header"; -import "./app.scss"; +import React from 'react'; +import Home from '@components/home/Home'; +import Header from '@components/header/Header'; +import './app.scss'; -const App: React.FC = () => { +const App: React.FC = (): JSX.Element => { return (
diff --git a/src/components/app/app.scss b/src/components/app/app.scss index 224ff48..b7017fb 100644 --- a/src/components/app/app.scss +++ b/src/components/app/app.scss @@ -1,6 +1,6 @@ -#root { +.app { width: 80%; margin: 0 auto; - padding: 2rem; + padding: 2rem 0; text-align: center; } diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index 24a4517..6da32c4 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -1,78 +1,56 @@ -import React from 'react'; -import { useSinglePokemon } from '@components/hooks/useSinglePokemon'; +import { Indeterminate } from '@faculedesma/ledesma-lib'; +import React, { useState } from 'react'; +import { ISinglePokemonParsed } from 'src/types'; import './card.scss'; -interface IPokemonCardProps { - pokemonId: string; +interface ICardProps { + pokemon: ISinglePokemonParsed; } -export const PokemonCard = ({ pokemonId }: IPokemonCardProps): JSX.Element => { - const { data, isLoading, isError } = useSinglePokemon(pokemonId); +export const Card: React.FC = ({ pokemon }): JSX.Element => { + const [isImageLoaded, setIsImageLoaded] = useState(false); - if (isLoading) { - return
Loading...
; - } + const handleImageLoaded = (): void => setIsImageLoaded(true); - if (isError) { - return
Error!
; - } - - const pokemon = data; - - return ( -
-
+ return pokemon !== undefined ? ( +
+
- header-card + poke-default + {!isImageLoaded && }
-
-
+
+
{pokemon?.name} - - {pokemon?.stats.hp}Hp - + {pokemon?.stats.hp}Hp
-
- - {pokemon?.stats.experience}Exp - +
+ {pokemon?.stats.experience}Exp
-
-
- {pokemon?.stats.attack}K -

Attack

+
+
+ {pokemon?.stats.attack}K +

Attack

-
- {pokemon?.stats.specialAttack}K -

Special

+
+ {pokemon?.stats.specialAttack}K +

Special

-
- {pokemon?.stats.defense}K -

Defense

+
+ {pokemon?.stats.defense}K +

Defense

+ ) : ( + <>No pokemon selected ); }; -export default PokemonCard; +export default Card; diff --git a/src/components/card/card.scss b/src/components/card/card.scss index b9866f6..209c54c 100644 --- a/src/components/card/card.scss +++ b/src/components/card/card.scss @@ -1,43 +1,77 @@ -.card { +.card, +.card-loading, +.card-empty { height: 400px; width: 300px; - border: 1px solid black; - border-radius: 5px; - background-color: white; + border: 2px solid black; + border-radius: 8px; + background-color: #FFFCF3; + box-shadow: 0px 4px 0px black; + + &-empty { + display: flex; + align-items: center; + justify-content: center; + } &-header { height: 30%; - background-color: yellow; + background-color: #FFCD69; display: flex; align-items: center; justify-content: center; - border-bottom: 1px solid black; + border-bottom: 2px solid black; + border-radius: 8px 8px 0 0; .image-container { position: relative; - width: 40%; - height: 100%; + width: 150px; + height: 150px; border-radius: 50%; - border: 1.5px solid black; - transform: translateY(50%); + border: 2px solid black; + transform: translateY(37%); + background-color: #FFFCF3; + img { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - clip-path: circle(50%); - border-radius: 50%; + top: 10%; + left: 10%; + width: 80%; + height: 80%; + } + + .indeterminate { + position: absolute; + transform: translate(-50%, 50%); } } } &-content { height: 45%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; + position: relative; + + .top { + position: absolute; + width: 100%; + top: 50%; + + .hp { + margin-left: 10px; + opacity: 0.5; + } + } + + .bottom { + position: absolute; + width: 100%; + top: 65%; + + .exp { + transform: translateY(40%); + opacity: 0.5; + } + } } &-footer { @@ -46,6 +80,23 @@ display: flex; align-items: center; justify-content: space-around; - border-top: 1px solid black; + border-top: 2px solid black; + + .stat { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + b { + font-size: 16px; + } + + p { + font-size: 12px; + opacity: 0.5; + } + } } } \ No newline at end of file diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index 582dda1..94a9a12 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -3,7 +3,7 @@ import PokemonLogo from '@assets/images/pokemon-logo.png'; import PokemonPikachu from '@assets/images/pokemon-svg.png'; import './header.scss'; -const Header = (): JSX.Element => { +const Header: React.FC = (): JSX.Element => { return (
poke-logo diff --git a/src/components/home/Home.tsx b/src/components/home/Home.tsx index 867fb43..2280db9 100644 --- a/src/components/home/Home.tsx +++ b/src/components/home/Home.tsx @@ -1,110 +1,19 @@ import React, { useState } from 'react'; -import { - Button, - Table, - TableRow, - TableHeaderCell, - TableCell -} from '@faculedesma/ledesma-lib'; -import PokemonCard from '@components/card/Card'; -import { usePokemons } from '@components/hooks/usePokemons'; -import { capitalizeFirstLetter } from '@utils/utils'; +import HomeTable from './HomeTable'; +import HomeCard from './HomeCard'; import './home.scss'; -const getPokemonIdFromURL = (url: string): string => { - const parts = url.split('/'); - return parts[parts.length - 2]; -}; - -const Home = (): JSX.Element => { - const [offset, setOffset] = useState(0); - const [selectedPokemonId, setSelectedPokemonId] = useState< - string | undefined - >(undefined); - - const { data, isLoading, isError } = usePokemons(offset); - - if (isLoading) { - return
Loading...
; - } - - if (isError) { - return
Error!
; - } - - const handleSelectRow = (id: string): void => setSelectedPokemonId(id); - - const handlePrev = (): void => setOffset(offset - 10); - - const handleNext = (): void => setOffset(offset + 10); - - if (data !== undefined) { - const columns = Object.keys(data.results[0]).map((column) => - capitalizeFirstLetter(column) - ); - const rows = data.results.map((row: any) => [ - capitalizeFirstLetter(row.name), - row.url - ]); +const Home: React.FC = (): JSX.Element => { + const [selectedPokemonId, setSelectedPokemonId] = useState(''); - return ( -
- - <> - - - <> - {columns?.map((name, columnIndex) => { - return ( - - {name} - - ); - })} - - - - - {rows?.map((row: any, rowIndex: number) => { - return ( - handleSelectRow(getPokemonIdFromURL(row[1]))} - > - <> - {row.map((data: any, rowDataIndex: number) => { - return {data}; - })} - - - ); - })} - - -
-
- - - Page: {offset / 10 + 1} - Pages: {Math.trunc(data.count / 10) + 1} -
- {selectedPokemonId != null ? ( -
- -
- ) : null} -
- ); - } + const handleSelectPokemonId = (id: string): void => setSelectedPokemonId(id); - return <>; + return ( +
+ + +
+ ); }; export default Home; diff --git a/src/components/home/HomeCard.tsx b/src/components/home/HomeCard.tsx new file mode 100644 index 0000000..c2c80ff --- /dev/null +++ b/src/components/home/HomeCard.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import Card from '@components/card/Card'; +import { useSinglePokemon } from '@components/hooks/useSinglePokemon'; + +interface IPokemonCardProps { + pokemonId: string; +} + +const EmptyCard: React.FC = (): JSX.Element => ( +
No pokemon selected
+); + +const LoadingCard: React.FC = (): JSX.Element => ( +
+
+
+
+
+
+
+); + +const HomeCard: React.FC = ({ pokemonId }): JSX.Element => { + const { data, isLoading, isError } = useSinglePokemon(pokemonId); + + if (pokemonId === '') { + return ; + } + + if (isLoading) { + return ; + } + + if (isError) { + return
Error!
; + } + + return ( +
+ +
+ ); +}; + +export default HomeCard; diff --git a/src/components/home/HomeTable.tsx b/src/components/home/HomeTable.tsx new file mode 100644 index 0000000..8819ba8 --- /dev/null +++ b/src/components/home/HomeTable.tsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import { + Table, + TableRow, + TableHeaderCell, + TableCell, + TableSkeleton, + Pagination +} from '@faculedesma/ledesma-lib'; +import { usePokemons } from '@components/hooks/usePokemons'; +import { capitalizeFirstLetter, getPokemonIdFromURL } from '@utils/utils'; +import './home.scss'; + +interface IHomeTableProps { + setPokemonId: (id: string) => void; +} + +const TableLoading: React.FC = (): JSX.Element => ; + +const HomeTable: React.FC = ({ + setPokemonId +}): JSX.Element => { + const [offset, setOffset] = useState(0); + let count: number = 0; + let rows: string[][] = []; + let columns: string[] = []; + + const { data, isLoading, isError } = usePokemons(offset); + + if (data !== undefined) { + columns = ['N#', capitalizeFirstLetter(Object.keys(data.results[0])[0])]; + rows = data.results.map((row: any) => [ + getPokemonIdFromURL(row.url), + capitalizeFirstLetter(row.name) + ]); + count = data.count; + } + + if (isError) { + return
Error!
; + } + + const handleSelectRow = (id: string): void => setPokemonId(id); + + const handlePrev = (): void => setOffset(offset - 10); + + const handleNext = (): void => setOffset(offset + 10); + + return ( +
+ {isLoading ? ( + + ) : ( + + <> + + + <> + {columns.map((name, columnIndex) => { + return ( + + {name} + + ); + })} + + + + + {rows.map((row, rowIndex) => { + return ( + handleSelectRow(row[0])} + > + <> + {row.map((data, rowDataIndex) => { + return {data}; + })} + + + ); + })} + + +
+ )} + +
+ ); +}; + +export default HomeTable; diff --git a/src/components/home/home.scss b/src/components/home/home.scss index cc5500b..9888122 100644 --- a/src/components/home/home.scss +++ b/src/components/home/home.scss @@ -1,7 +1,57 @@ .home { - width: 100%; + min-width: 100%; + min-height: 100%; display: flex; - flex-direction: column; align-items: center; - justify-content: center; + justify-content: space-evenly; + margin-top: 40px; + + &-table { + width: 60%; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + + .table { + background-color: #fffcf3; + + thead { + th { + &:first-child { + width: 60px; + } + } + } + + tbody { + tr { + cursor: pointer; + + &:hover { + background-color: #fbd68b; + } + + &:active { + background-color: #ffcd69; + } + } + } + } + + .table-skeleton { + height: 528px; + background-color: #fffcf3; + } + + .pagination { + margin-top: 20px; + } + } + + &-card { + display: flex; + align-items: center; + justify-content: center; + } } diff --git a/src/components/hooks/useSinglePokemon.ts b/src/components/hooks/useSinglePokemon.ts index bcb4827..039d458 100644 --- a/src/components/hooks/useSinglePokemon.ts +++ b/src/components/hooks/useSinglePokemon.ts @@ -36,7 +36,10 @@ export const useSinglePokemon = ( ): IUseSinglePokemonResponse => { const { data, isError, isLoading } = useQuery( ['pokemon', pokemonId], - getPokemon + getPokemon, + { + enabled: pokemonId !== '' + } ); return { data, isError, isLoading }; }; diff --git a/src/index.scss b/src/index.scss index 38d44a1..a11c1cf 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,21 +1,12 @@ -:root { +body { font-family: Inter, Avenir, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; - - color-scheme: light dark; - // background-color: rgba(255, 255, 255, 0.87); - color: #242424; -} - -body { + width: 100%; margin: 0; - display: flex; - place-items: center; - min-width: 320px; min-height: 100vh; - background-color: white; + color: #242424; } h1 { diff --git a/src/main.tsx b/src/main.tsx index e2d76f9..1af1b5c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,13 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "@components/app/App"; -import { QueryClient, QueryClientProvider } from "react-query"; -import { ReactQueryDevtools } from "react-query/devtools"; -import "./index.scss"; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from '@components/app/App'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { ReactQueryDevtools } from 'react-query/devtools'; +import './index.scss'; const queryClient = new QueryClient(); -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( diff --git a/src/utils/_variables.scss b/src/utils/_variables.scss new file mode 100644 index 0000000..10ee0ae --- /dev/null +++ b/src/utils/_variables.scss @@ -0,0 +1,2 @@ +// colors +$primary-colors: #fdf8e3; \ No newline at end of file diff --git a/src/utils/utils.ts b/src/utils/utils.ts index a7af4a4..b2f32ec 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,2 +1,7 @@ export const capitalizeFirstLetter = (word: string): string => word.charAt(0).toUpperCase() + word.slice(1); + +export const getPokemonIdFromURL = (url: string): string => { + const parts = url.split('/'); + return parts[parts.length - 2]; +}; diff --git a/test/Home.spec.tsx b/test/HomeTable.spec.tsx similarity index 78% rename from test/Home.spec.tsx rename to test/HomeTable.spec.tsx index 57cfc54..147f08e 100644 --- a/test/Home.spec.tsx +++ b/test/HomeTable.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Home from '../src/components/home/Home'; +import HomeTable from '../src/components/home/HomeTable'; import { usePokemons } from '../src/components/hooks/usePokemons'; import * as hooks from '../src/components/hooks/usePokemons'; import { render } from '@testing-library/react'; @@ -23,26 +23,30 @@ const mockedData = { ] }; -describe('', () => { +const homeTableProps = { + setPokemonId: jest.fn() +}; + +describe('', () => { it('fetches all pokemons', async () => { jest.spyOn(hooks, 'usePokemons').mockImplementation(() => ({ data: mockedData, isError: false, isLoading: false })); - await render(); + await render(); expect(usePokemons).toHaveBeenCalledTimes(1); expect(usePokemons).toHaveBeenCalledWith(0); }); - it('shows loading spinner', async () => { + it('shows table skeleton on loading', async () => { jest.spyOn(hooks, 'usePokemons').mockImplementation((offset) => ({ data: undefined, isError: false, isLoading: true })); - const { getByText } = await render(); - expect(getByText('Loading...')).toBeInTheDocument(); + const { container } = await render(); + expect(container.firstChild?.firstChild).toHaveClass('table-skeleton'); }); it('shows error', async () => { @@ -51,7 +55,7 @@ describe('', () => { isError: true, isLoading: false })); - const { getByText } = await render(); + const { getByText } = await render(); expect(getByText('Error!')).toBeInTheDocument(); }); });