Skip to content

Commit

Permalink
Merge pull request #19 from Filgeary:feat/add-single-character-page
Browse files Browse the repository at this point in the history
feat: add single character page
  • Loading branch information
Filgeary authored Nov 10, 2023
2 parents 8176748 + 8521387 commit 8e41120
Show file tree
Hide file tree
Showing 24 changed files with 364 additions and 25 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
>
> for fun & learning purpose 😃
Marvel Portal with Characters / Comics info using official Marvel API
Marvel Portal with Characters / Comics using official Marvel API

## Tech Stack

Expand All @@ -29,7 +29,7 @@ Marvel Portal with Characters / Comics info using official Marvel API
- Random Character block
- Characters List with Character Info
- Comics List
- Pages: Characters, Comics/SingleComic
- Pages: Characters/SingleCharacter, Comics/SingleComic

## Dev Features

Expand All @@ -39,7 +39,6 @@ Marvel Portal with Characters / Comics info using official Marvel API

## TODO

- [ ] add Single Character Page ?
- [ ] Rewrite to TS ?
- [ ] Fetch lib like React Query ?
- [ ] New Features like Favorites, Search, complex Filters ?
Expand Down
5 changes: 5 additions & 0 deletions src/App/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import AppLayout from '../layout/AppLayout'

const HomePage = lazy(() => import('../pages/HomePage'))
const CharactersPage = lazy(() => import('../pages/CharactersPage'))
const SingleCharacterPage = lazy(() => import('../pages/SingleCharacterPage'))
const ComicsPage = lazy(() => import('../pages/ComicsPage'))
const SingleComicPage = lazy(() => import('../pages/SingleComicPage'))
const NotFound404 = lazy(() => import('../components/_shared/NotFound404'))
Expand All @@ -21,6 +22,10 @@ const App = () => {
path='/characters'
element={<CharactersPage />}
/>
<Route
path='/characters/:id'
element={<SingleCharacterPage />}
/>

<Route
path='/comics'
Expand Down
15 changes: 12 additions & 3 deletions src/components/CharInfo/CharInfo.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from 'react'
import { ArrowLeft } from 'react-feather'
import { Link } from 'react-router-dom'
import { IMAGE_VARIANT } from '../../constants'
import { transformCharacter } from '../../utils/apiAdapter'
import Skeleton from '../_shared/Skeleton'
Expand All @@ -12,15 +14,15 @@ const CharInfo = ({ char }) => {
if (!char) {
return (
<section className='d-grid align-items-start sticky top-1 p-0'>
<h2 style={{ paddingLeft: '1.5rem', color: 'var(--cra-font-light)' }}>
Select a Character, please
<h2 className={styles.selectCharHeading}>
<ArrowLeft /> Select a Character, please
</h2>
<Skeleton />
</section>
)
}

const { name, description, thumbnail, comics } =
const { id, name, description, thumbnail, comics } =
transformCharacter(char, IMAGE_VARIANT['200x200']) ?? {}

return (
Expand All @@ -45,6 +47,13 @@ const CharInfo = ({ char }) => {
</div>
</header>

<Link
to={`/characters/${id}`}
className='btn btn-primary btn-shadow-dark'
>
Go to Character Page
</Link>

<p data-testid='charInfo-description'>{description}</p>

<section className='p-0 d-flex flex-column gap-1'>
Expand Down
7 changes: 7 additions & 0 deletions src/components/CharInfo/CharInfo.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
.selectCharHeading {
display: flex;
gap: 1rem;
padding-left: 1.5rem;
color: var(--cra-font-light);
}

.article {
display: grid;
align-items: start;
Expand Down
8 changes: 6 additions & 2 deletions src/components/CharInfo/CharInfo.test.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
// import userEvent from '@testing-library/user-event'
import React from 'react'
import { MemoryRouter } from 'react-router-dom'
import charResponseJSON from '../../__fixtures/api/characterById.json'
import CharInfo from './CharInfo'

Expand All @@ -11,7 +11,11 @@ const charResponseObj = JSON.parse(JSON.stringify(charResponseJSON))
const char = charResponseObj.data?.results?.at(0)

const initRender = char => {
render(<CharInfo char={char} />)
render(
<MemoryRouter>
<CharInfo char={char} />
</MemoryRouter>,
)
}

describe('CharInfo', () => {
Expand Down
44 changes: 44 additions & 0 deletions src/components/CharacterProfile/CharacterProfile.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react'
import { IMAGE_VARIANT } from '../../constants'
import { transformCharacter } from '../../utils/apiAdapter'
import styles from './CharacterProfile.module.css'

/**
* @param {object} props
* @param {import('../../types/ICharacter').ICharacter | null | undefined} props.character
*/
const CharacterProfile = ({ character }) => {
if (!character) return <h2>No Character!</h2>

const { name, thumbnail, description } =
transformCharacter(character, IMAGE_VARIANT['300x450']) ?? {}

return (
<article
data-testid='characterProfile'
className='p-3 d-grid align-items-start gap-5'
>
<div className={styles.wrapper}>
<figure>
<img
src={thumbnail}
alt={name}
width={300}
height={450}
/>
</figure>

<section className='d-flex flex-column gap-4 h-100 p-0 px-2 font-light'>
<h2 className='text-gradient f-size-250'>{name}</h2>
<p>{description}</p>
</section>
</div>

<hr
style={{ width: '60%', background: 'var(--cra-bg-grey)', border: 'none', height: '2px' }}
/>
</article>
)
}

export default CharacterProfile
21 changes: 21 additions & 0 deletions src/components/CharacterProfile/CharacterProfile.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.wrapper {
display: grid;
align-items: start;
grid-template-columns: 1fr 2fr;
}

@media (max-width: 1199px) {
/* desktop (<= 1199) */
}

@media (max-width: 1023px) {
/* tablet (<= 1023) */
}

@media (max-width: 767px) {
/* mobile -> tablet (<= 767) */
}

@media (max-width: 425px) {
/* only mobile (<= 425) */
}
27 changes: 27 additions & 0 deletions src/components/CharacterProfile/CharacterProfile.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import characterResponseJson from '../../__fixtures/api/characterById.json'
import CharacterProfile from './CharacterProfile'

/**
* @type {import('../../types/ICharacter').ICharacterDataWrapper}
*/
const characterResponseObj = JSON.parse(JSON.stringify(characterResponseJson))
const character = characterResponseObj?.data?.results?.at(0)

describe('CharacterProfile', () => {
it('should render properly', () => {
render(<CharacterProfile character={character} />)

expect(screen.getByTestId('characterProfile')).toBeInTheDocument()
expect(screen.getByRole('heading', { name: /guardians of the galaxy/i })).toBeInTheDocument()
expect(screen.getByRole('img', { name: /guardians of the galaxy/i })).toBeInTheDocument()

// full description
expect(
screen.getByText(
/a group of cosmic adventurers brought together by star-lord, the guardians of the galaxy protect the universe from threats all across space\. the team also includes drax, gamora, groot and rocket raccoon!/i,
),
).toBeInTheDocument()
})
})
2 changes: 2 additions & 0 deletions src/components/CharacterProfile/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import CharacterProfile from './CharacterProfile'
export default CharacterProfile
14 changes: 11 additions & 3 deletions src/components/RandomChar/RandomChar.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { IMAGE_VARIANT } from '../../constants'
import { transformCharacter } from '../../utils/apiAdapter'
import styles from './RandomChar.module.css'
Expand All @@ -8,11 +9,14 @@ import styles from './RandomChar.module.css'
* @param {import('../../types/ICharacter').ICharacter | null | undefined} props.char
*/
const RandomChar = ({ char }) => {
const { name, description, thumbnail } =
const { id, name, description, thumbnail } =
transformCharacter(char, IMAGE_VARIANT['250x250'], false) ?? {}

return (
<section className={styles.charSection}>
<Link
to={`/characters/${id}`}
className={styles.charLink}
>
<figure>
<img
data-testid='randomChar-thumbnail'
Expand All @@ -32,7 +36,11 @@ const RandomChar = ({ char }) => {
</h2>
<p data-testid='randomChar-description'>{description}</p>
</div>
</section>

<div className={styles.overlay}>
<p>Go To Character Page</p>
</div>
</Link>
)
}

Expand Down
23 changes: 21 additions & 2 deletions src/components/RandomChar/RandomChar.module.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
.charSection {
.charLink {
position: relative;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
min-height: 250px;
padding: 0;
font-family: system-ui, sans-serif;
color: var(--cra-font-dark);
background: var(--cra-bg-light);
}
.overlay {
display: none;
justify-content: center;
place-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-size: 2rem;
color: white;
background: rgba(0, 0, 0, 0.8);
}
.charLink:hover .overlay {
display: flex;
}

.charTitle {
background: var(--cra-accent-color);
Expand All @@ -25,7 +44,7 @@

@media (max-width: 767px) {
/* mobile -> tablet (<= 767) */
.charSection {
.charLink {
grid-template-columns: 1fr;
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/components/RandomChar/RandomChar.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import { MemoryRouter } from 'react-router-dom'
import charResponseJSON from '../../__fixtures/api/characterById.json'
import RandomChar from './RandomChar'

Expand All @@ -9,9 +10,17 @@ import RandomChar from './RandomChar'
const charResponseObj = JSON.parse(JSON.stringify(charResponseJSON))
const char = charResponseObj.data?.results?.at(0)

const initRender = () => {
render(
<MemoryRouter>
<RandomChar char={char} />
</MemoryRouter>,
)
}

describe('RandomChar', () => {
it('should render correctly with fixtures', () => {
render(<RandomChar char={char} />)
initRender()

expect(screen.getByRole('heading', { name: /guardians of the galaxy/i })).toBeInTheDocument()
expect(screen.getByRole('img', { name: /guardians of the galaxy/i })).toBeInTheDocument()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import CharacterProfile from '../../components/CharacterProfile'
import ErrorMessage from '../../components/_shared/ErrorMessage'
import Spinner from '../../components/_shared/Spinner'
import { useFetchCharacterById } from '../../hooks/useFetchCharacterById'

// import styles from './CharacterProfileContainer.module.css'

/**
* @param {object} props
* @param {string} props.id
*/
const CharacterProfileContainer = ({ id }) => {
const { responseData, isLoading, isError, errorMsg } = useFetchCharacterById(id)

// @ts-ignore
const character = responseData?.data?.results?.at(0) // TODO: add types

return (
<div style={{ minHeight: 250 }}>
{isLoading && <Spinner />}
{isError && <ErrorMessage text={errorMsg} />}
{!isError && !isLoading && <CharacterProfile character={character} />}
</div>
)
}

export default CharacterProfileContainer
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* styles */

@media (max-width: 1199px) {
/* desktop (<= 1199) */
}

@media (max-width: 1023px) {
/* tablet (<= 1023) */
}

@media (max-width: 767px) {
/* mobile -> tablet (<= 767) */
}

@media (max-width: 425px) {
/* only mobile (<= 425) */
}
Loading

0 comments on commit 8e41120

Please sign in to comment.