diff --git a/public/planet.png b/public/planet.png index 1c50a68..7a7da32 100644 Binary files a/public/planet.png and b/public/planet.png differ diff --git a/public/portal.png b/public/portal.png index 9c619d6..c550b12 100644 Binary files a/public/portal.png and b/public/portal.png differ diff --git a/src/components/CharacterCard.tsx b/src/components/CharacterCard.tsx new file mode 100644 index 0000000..8f70ffd --- /dev/null +++ b/src/components/CharacterCard.tsx @@ -0,0 +1,8 @@ +import { Character } from 'rickmortyapi'; +import MiniCard from './MiniCard'; + +export default function CharacterCard({ character }: { character: Character }) { + return ( + + ); +} \ No newline at end of file diff --git a/src/components/CharacterDetail.tsx b/src/components/CharacterDetail.tsx deleted file mode 100644 index a905419..0000000 --- a/src/components/CharacterDetail.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Character } from 'rickmortyapi'; -import { useLoaderData } from 'react-router-dom'; -import MiniCard from './MiniCard'; - -export default function CharacterDetail({ character }: { character?: Character }) { - const char = character || useLoaderData() as Character; - return ( - - ); -} \ No newline at end of file diff --git a/src/components/DetailsLayout.tsx b/src/components/DetailsLayout.tsx new file mode 100644 index 0000000..c156f16 --- /dev/null +++ b/src/components/DetailsLayout.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { Link } from "react-router-dom"; + +export type DetailFacts = { + type: string; + value: string; + url?: string; +} + +export default function DetailsLayout({ image, title, facts, childrenTitle, children }: { image: string, title: string, facts: DetailFacts[], childrenTitle: string, children?: React.ReactNode }) { + return ( + + + + + {title} + + + {facts.map((fact, index) => ( + + {fact.type} + + {fact.url ? ( + {fact.value} + ) : fact.value} + + + ))} + + + + + {childrenTitle}: + + {children} + + + + ); +} \ No newline at end of file diff --git a/src/components/EpisodeDetail.tsx b/src/components/EpisodeCard.tsx similarity index 90% rename from src/components/EpisodeDetail.tsx rename to src/components/EpisodeCard.tsx index b3fd524..a758d94 100644 --- a/src/components/EpisodeDetail.tsx +++ b/src/components/EpisodeCard.tsx @@ -9,7 +9,7 @@ import Season4 from "/seasons/s04.jpg"; import Season5 from "/seasons/s05.jpg"; import Season6 from "/seasons/s06.jpg"; -export default function EpisodeDetail({ episode }: { episode?: Episode }) { +export default function EpisodeCard({ episode }: { episode?: Episode }) { const ep = episode || useLoaderData() as Episode; const season = parseInt(ep.episode.slice(2, 3), 10); const images = [Season1, Season2, Season3, Season4, Season5, Season6]; diff --git a/src/components/LocationDetail.tsx b/src/components/LocationCard.tsx similarity index 83% rename from src/components/LocationDetail.tsx rename to src/components/LocationCard.tsx index f8f411b..c5bcd52 100644 --- a/src/components/LocationDetail.tsx +++ b/src/components/LocationCard.tsx @@ -4,7 +4,7 @@ import MiniCard from "./MiniCard"; import PortalImage from "/portal.png" import PlanetImage from "/planet.png" -export default function LocationDetail({ location }: { location?: Location }) { +export default function LocationCard({ location }: { location?: Location }) { const place = location || useLoaderData() as Location; return ( diff --git a/src/loaders.ts b/src/loaders.ts index c8dc638..0fc6eef 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -44,4 +44,13 @@ export async function episodesLoader({ request }: { request: Request }) { export async function episodeDetailLoader({ params }: { params: Params<"episodeId"> }) { const response = await getEpisode(parseInt(params.episodeId ?? "", 10)) return response.data +} + +export function parseAPIId(location: string) { + try { + const url = new URL(location); + return parseInt(url.pathname.split('/').pop() || '', 10); + } catch (error) { + return -1; + } } \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 50ca3a8..104919d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,12 +5,12 @@ import { charactersLoader, characterDetailLoader, episodesLoader, episodeDetailL import Root from './routes/Root.tsx' import Characters from './routes/Characters.tsx' -import CharacterDetail from './components/CharacterDetail.tsx' -import EpisodeDetail from './components/EpisodeDetail.tsx' import Episodes from './routes/Episodes.tsx' import Locations from './routes/Locations.tsx' -import LocationDetail from './components/LocationDetail.tsx' import './index.css' +import CharacterDetails from './routes/CharacterDetails.tsx' +import EpisodeDetails from './routes/EpisodeDetails.tsx' +import LocationDetails from './routes/LocationDetails.tsx' const router = createBrowserRouter([ { @@ -30,7 +30,7 @@ const router = createBrowserRouter([ }, { path: '/characters/:characterId', - element: , + element: , loader: characterDetailLoader }, { @@ -40,7 +40,7 @@ const router = createBrowserRouter([ }, { path: '/locations/:locationId', - element: , + element: , loader: locationDetailLoader }, { @@ -50,7 +50,7 @@ const router = createBrowserRouter([ }, { path: '/episodes/:episodeId', - element: , + element: , loader: episodeDetailLoader }, ] diff --git a/src/routes/CharacterDetails.tsx b/src/routes/CharacterDetails.tsx new file mode 100644 index 0000000..85782bb --- /dev/null +++ b/src/routes/CharacterDetails.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Link, useLoaderData } from "react-router-dom"; +import { Character, Episode, getEpisode } from "rickmortyapi"; +import { parseAPIId } from "../loaders"; +import EpisodeCard from "../components/EpisodeCard"; +import DetailsLayout, { DetailFacts } from "../components/DetailsLayout"; + +export default function CharacterDetails() { + const char = useLoaderData() as Character; + const [episodes, setEpisodes] = React.useState([]); + const originId = parseAPIId(char.origin.url); + const locationId = parseAPIId(char.location.url); + + const facts: DetailFacts[] = [ + { type: "Species", value: char.species }, + { type: "Status", value: char.status }, + { type: "Gender", value: char.gender }, + { type: "Origin", value: char.origin.name, url: originId !== -1 ? `/locations/${originId}` : undefined }, + { type: "Location", value: char.location.name, url: locationId !== -1 ? `/locations/${locationId}` : undefined } + ] + + React.useEffect(() => { + const episodeList = char.episode.map((episode) => parseAPIId(episode)); + getEpisode(episodeList).then((data) => { + if (data.status === 200 && data.data) { + if (data.data.length) { + setEpisodes(data.data) + } else if (typeof data.data === 'object') { + // XXX: Wrong type from client library, when there's only one result the type is Episode and not Episode[] + // @ts-ignore + setEpisodes([data.data]); + } + } + }) + }, [episodes]) + + return ( + + {episodes.length > 0 ? episodes.map((episode, index) => ( + + + + )) : "Loading..."} + + ); +} \ No newline at end of file diff --git a/src/routes/Characters.tsx b/src/routes/Characters.tsx index c2c7b61..5fb774c 100644 --- a/src/routes/Characters.tsx +++ b/src/routes/Characters.tsx @@ -1,6 +1,6 @@ import { Character } from 'rickmortyapi'; import { Link, useLoaderData, useSearchParams } from 'react-router-dom'; -import CharacterDetail from '../components/CharacterDetail'; +import CharacterCard from '../components/CharacterCard'; import Pagination from '../components/Pagination'; export default function Characters() { @@ -14,7 +14,7 @@ export default function Characters() { {characters.map((character) => ( - + ))} diff --git a/src/routes/EpisodeDetails.tsx b/src/routes/EpisodeDetails.tsx new file mode 100644 index 0000000..6dc112f --- /dev/null +++ b/src/routes/EpisodeDetails.tsx @@ -0,0 +1,52 @@ +import { Character, Episode, getCharacter } from "rickmortyapi"; +import DetailsLayout, { DetailFacts } from "../components/DetailsLayout"; +import { Link, useLoaderData } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { parseAPIId } from "../loaders"; +import CharacterCard from "../components/CharacterCard"; + +import Season1 from "/seasons/s01.jpg"; +import Season2 from "/seasons/s02.jpg"; +import Season3 from "/seasons/s03.jpg"; +import Season4 from "/seasons/s04.jpg"; +import Season5 from "/seasons/s05.jpg"; +import Season6 from "/seasons/s06.jpg"; + +export default function EpisodeDetails() { + const episode = useLoaderData() as Episode; + const season = parseInt(episode.episode.slice(2, 3), 10); + const images = [Season1, Season2, Season3, Season4, Season5, Season6]; + const image = season <= 6 ? images[season - 1] : images[0]; + const [characters, setCharacters] = useState([]); + + const DetailFacts: DetailFacts[] = [ + { type: "Episode", value: episode.episode }, + { type: "Aired", value: episode.air_date }, + { type: "Characters", value: `${episode.characters.length} characters` }, + ] + + useEffect(() => { + const characterList = episode.characters.map((episode) => parseAPIId(episode)); + getCharacter(characterList).then((data) => { + if (data.status === 200 && data.data) { + if (data.data.length) { + setCharacters(data.data) + } else if (typeof data.data === 'object') { + // XXX: Wrong type from client library, when there's only one result the type is Episode and not Episode[] + // @ts-ignore + setCharacters([data.data]); + } + } + }) + }, [characters]) + + return ( + + {characters.length > 0 ? characters.map((character, index) => ( + + + + )) : "Loading..."} + + ) +} \ No newline at end of file diff --git a/src/routes/Episodes.tsx b/src/routes/Episodes.tsx index 86084e6..7203037 100644 --- a/src/routes/Episodes.tsx +++ b/src/routes/Episodes.tsx @@ -1,6 +1,6 @@ import { Link, useLoaderData, useSearchParams } from "react-router-dom"; import { Episode } from "rickmortyapi"; -import EpisodeDetail from "../components/EpisodeDetail"; +import EpisodeCard from "../components/EpisodeCard"; import Pagination from "../components/Pagination"; export default function Episodes() { @@ -14,7 +14,7 @@ export default function Episodes() { {episodes.map((episode) => ( - + ))} diff --git a/src/routes/LocationDetails.tsx b/src/routes/LocationDetails.tsx new file mode 100644 index 0000000..8467da7 --- /dev/null +++ b/src/routes/LocationDetails.tsx @@ -0,0 +1,43 @@ +import { Link, useLoaderData } from "react-router-dom"; +import DetailsLayout, { DetailFacts } from "../components/DetailsLayout"; +import { Character, getCharacter, Location } from "rickmortyapi"; +import PortalImage from "/portal.png" +import PlanetImage from "/planet.png" +import { useEffect, useState } from "react"; +import { parseAPIId } from "../loaders"; +import CharacterCard from "../components/CharacterCard"; + +export default function LocationDetails() { + const location = useLoaderData() as Location; + const facts:DetailFacts[] = [ + { type: "Type", value: location.type }, + { type: "Dimension", value: location.dimension }, + { type: "Residents", value: `${location.residents.length} residents` } + ] + const [characters, setCharacters] = useState([]); + + useEffect(() => { + const characterList = location.residents.map((resident) => parseAPIId(resident)); + getCharacter(characterList).then((data) => { + if (data.status === 200 && data.data) { + if (data.data.length) { + setCharacters(data.data) + } else if (typeof data.data === 'object') { + // XXX: Wrong type from client library, when there's only one result the type is Episode and not Episode[] + // @ts-ignore + setCharacters([data.data]); + } + } + }) + }, [characters]) + + return ( + + {characters.length > 0 ? characters.map((character, index) => ( + + + + )) : "Loading..."} + + ); +} \ No newline at end of file diff --git a/src/routes/Locations.tsx b/src/routes/Locations.tsx index 10f2756..2984bb1 100644 --- a/src/routes/Locations.tsx +++ b/src/routes/Locations.tsx @@ -1,6 +1,6 @@ import { Link, useLoaderData, useSearchParams } from "react-router-dom"; import { Location } from "rickmortyapi"; -import LocationDetail from "../components/LocationDetail"; +import LocationCard from "../components/LocationCard"; import Pagination from "../components/Pagination"; export default function Locations() { @@ -14,7 +14,7 @@ export default function Locations() { {locations.map((location) => ( - + ))}