-
+
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}
+
+ ))}
+
+
+
+
+
+

+

+
+
+
+
+
+ )
+}
\ 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 && (
+