diff --git a/src/assets/X.svg b/src/assets/X.svg new file mode 100644 index 0000000..b246dcf --- /dev/null +++ b/src/assets/X.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/arrow-left.svg b/src/assets/arrow-left.svg new file mode 100644 index 0000000..0b5ced9 --- /dev/null +++ b/src/assets/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/arrow-right.svg b/src/assets/arrow-right.svg new file mode 100644 index 0000000..0b847d0 --- /dev/null +++ b/src/assets/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/detail-card-shape.svg b/src/assets/detail-card-shape.svg new file mode 100644 index 0000000..30d3da3 --- /dev/null +++ b/src/assets/detail-card-shape.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/CharacterCardSmall.tsx b/src/components/CharacterCardSmall.tsx index 756030a..39c7a36 100644 --- a/src/components/CharacterCardSmall.tsx +++ b/src/components/CharacterCardSmall.tsx @@ -5,9 +5,10 @@ import StarShape from '../assets/star.svg' type CharacterCardSmallProps = { name: string; image: string; + onDetails: () => void; }; -export default function CharacterCardSmall({name, image}: CharacterCardSmallProps){ +export default function CharacterCardSmall({name, image, onDetails}: CharacterCardSmallProps){ return(
@@ -15,7 +16,7 @@ export default function CharacterCardSmall({name, image}: CharacterCardSmallProp character image

{name}

- + diff --git a/src/components/CharacterDetailCard.css b/src/components/CharacterDetailCard.css new file mode 100644 index 0000000..2d0fd84 --- /dev/null +++ b/src/components/CharacterDetailCard.css @@ -0,0 +1,242 @@ +.detail-overlay { + position: fixed; + inset: 0; + z-index: 10; + + display: flex; + align-items: center; + justify-content: center; + + background-color: rgb(0 0 0 / 75%); +} + +.detail-card { + position: relative; + display: flex; + gap: 2rem; + + width: 60rem; + max-width: 90%; + padding: 3rem; + + color: white; + background-color: #5A7D0D; + border-radius: 1.5rem; +} + +.detail-info p { + font-weight: 400; +} + +.detail-info strong { + font-weight: 800; +} + +.character-facts { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem 2rem; +} + +.character-facts p { + margin: 0; + padding: 0; +} + +.detail-info h2 { + margin-top: 0; + color: white; + font-size: 3rem; + font-weight: bold; + text-transform: uppercase; +} + +.detail-image-frame { + position: relative; + display: flex; + align-items: center; + justify-content: center; + + width: 45%; + margin-left: auto; + transform: translateX(0.5rem); + + aspect-ratio: 1 / 0.93; + align-self: center; +} + +.detail-card-shape { + position: absolute; + width: 100%; + height: 100%; +} + +.detail-character-image { + position: relative; + z-index: 1; + + width: 90%; + height: 90%; + object-fit: cover; + border-radius: 1rem; +} + +.close-button { + position: absolute; + top: 1rem; + right: 1rem; + + padding: 0; + border: none; + background: transparent; + cursor: pointer; +} + +.close-button img { + display: block; + width: 1.5rem; + height: 1.5rem; +} + +.detail-episodes { + margin-top: 2rem; +} + +.detail-episodes h3 { + margin-bottom: 0.5rem; + font-size: 2rem; + text-transform: uppercase; +} + +.detail-episodes ul { + max-height: 10rem; + margin: 0; + padding-left: 1rem; + overflow-y: auto; + list-style: none; +} + +.detail-episodes li { + margin-bottom: 0.4rem; +} + +.previous-button, +.next-button { + position: absolute; + top: 50%; + z-index: 11; + + border: none; + background: transparent; + cursor: pointer; + transform: translateY(-50%); +} + +.previous-button { + left: 2rem; +} + +.next-button { + right: 2rem; +} + +.navigation-arrow { + width: 3rem; + height: 3rem; +} + +.detail-heading { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.detail-favorite-button { + flex-shrink: 0; + padding: 0; + border: none; + background: transparent; + cursor: pointer; +} + +.detail-star { + display: block; + width: 2.5rem; + height: 2.5rem; + filter: brightness(0) invert(1); +} + +@media (max-width: 600px) { + .detail-overlay { + padding: 1rem; + box-sizing: border-box; + } + + .detail-card { + flex-direction: column; + + width: 100%; + max-width: 100%; + max-height: 90vh; + padding: 3rem 1.5rem 1.5rem; + box-sizing: border-box; + + overflow-y: auto; + } + + .detail-card-shape { + transform: scaleY(1.03); + transform-origin: center; + } + + .detail-info h2 { + font-size: 2rem; + overflow-wrap: anywhere; + } + + .detail-info p { + font-size: 1rem; + overflow-wrap: anywhere; + } + + .detail-character-image { + width: 100%; + height: 100%; + } + + .detail-image-frame { + width: 85%; + margin: 0 auto; + padding: 2rem; + box-sizing: border-box; + transform: none; + aspect-ratio: 1 / 1; + } + + .detail-episodes h3 { + font-size: 1.5rem; + } + + .detail-episodes li { + font-size: 1rem; + } + + .previous-button, + .next-button { + padding: 0; + } + + .previous-button { + left: 1.2rem; + } + + .next-button { + right: 1.2rem; + } + + .navigation-arrow { + width: 2.25rem; + height: 2.25rem; + } +} \ No newline at end of file diff --git a/src/components/CharacterDetailCard.tsx b/src/components/CharacterDetailCard.tsx new file mode 100644 index 0000000..6fe547c --- /dev/null +++ b/src/components/CharacterDetailCard.tsx @@ -0,0 +1,99 @@ +import "./CharacterDetailCard.css"; +import { useEffect, useState } from "react" + +import type { Character } from "../types/Character"; +import DetailCardShape from '../assets/detail-card-shape.svg' +import ArrowLeft from '../assets/arrow-left.svg' +import ArrowRight from '../assets/arrow-right.svg' +import X from '../assets/X.svg' +import Star from '../assets/star.svg' + +type CharacterDetailCardProps = { + character: Character; + onClose: () => void; + onPrevious: () => void; + onNext: () => void; +} + +type Episode = { + id: number; + name: string; + episode: string; +} + +export default function CharacterDetailCard({ character, onClose, onPrevious, onNext }: CharacterDetailCardProps) { + + const [episodes, setEpisodes] = useState([]); + + // AI was used to create this useEffect function. + useEffect(() => { + async function fetchEpisodes() { + try { + const episodeData = await Promise.all( + character.episode.map(async (episodeUrl) => { + const response = await fetch(episodeUrl); + if (!response.ok) { + throw new Error(`Could not fetch episode: ${response.status}`) + } + return response.json(); + }) + ); + setEpisodes(episodeData); + } + catch (error) { + console.error(error); + } + } + fetchEpisodes(); + }, [character]) + + return ( +
+ + +
+ + +
+
+

{character.name}

+ +
+ +
+

Gender: {character.gender}

+

Origin: {character.origin.name}

+

Species: {character.species}

+

Status: {character.status}

+
+ +
+

Episodes

+
    + {episodes.map((episode) => ( +
  • + {episode.episode} - {episode.name} +
  • + ))} +
+
+
+ +
+ + {character.name} +
+
+ + +
+ ) +} \ No newline at end of file diff --git a/src/pages/CharactersPage.tsx b/src/pages/CharactersPage.tsx index b654f7d..1204ce6 100644 --- a/src/pages/CharactersPage.tsx +++ b/src/pages/CharactersPage.tsx @@ -2,17 +2,10 @@ import './CharactersPage.css' import { useEffect, useState } from "react"; import CharacterCardSmall from "../components/CharacterCardSmall"; +import CharacterDetailCard from "../components/CharacterDetailCard"; import SearchIcon from "../assets/search.svg" -type Character = { - id: number; - name: string; - species: string; - status: string; - gender: string; - origin: string; - image: string; -}; +import type { Character } from "../types/Character"; const filters = [ {title: 'Gender', options: ['Female', 'Male', 'Unknown'] }, @@ -23,6 +16,7 @@ const filters = [ export default function CharactersPage(){ const [characters, setCharacters] = useState([]); + const [selectedCharacter, setSelectedCharacter] = useState(null); useEffect(() => { async function fetchCharacters(){ @@ -43,6 +37,32 @@ export default function CharactersPage(){ fetchCharacters() }, []) + function showPreviousCharacter(){ + if (!selectedCharacter) return; + + const currentIndex = characters.findIndex( + (character) => character.id === selectedCharacter.id + ); + + const previousIndex = + currentIndex === 0 ? characters.length - 1 : currentIndex - 1; + + setSelectedCharacter(characters[previousIndex]); + } + + function showNextCharacter(){ + if (!selectedCharacter) return; + + const currentIndex = characters.findIndex( + (character) => character.id === selectedCharacter.id + ); + + const nextIndex = + currentIndex === characters.length - 1 ? 0 : currentIndex + 1; + + setSelectedCharacter(characters[nextIndex]); + } + return(
@@ -58,6 +78,7 @@ export default function CharactersPage(){ key={character.id} name={character.name} image={character.image} + onDetails={() => setSelectedCharacter(character)} /> ))}
@@ -76,6 +97,14 @@ export default function CharactersPage(){ ))}
+ {selectedCharacter && ( + setSelectedCharacter(null)} + onPrevious={showPreviousCharacter} + onNext={showNextCharacter} + /> + )} ) } \ No newline at end of file diff --git a/src/types/Character.ts b/src/types/Character.ts new file mode 100644 index 0000000..39eb0ea --- /dev/null +++ b/src/types/Character.ts @@ -0,0 +1,13 @@ +export type Character = { + id: number; + name: string; + species: string; + status: string; + gender: string; + origin: { + name: string; + url: string; + } + image: string; + episode: string[]; +} \ No newline at end of file