Skip to content

Commit

Permalink
add: Implement complete detail pages
Browse files Browse the repository at this point in the history
  • Loading branch information
lyudmil-mitev committed Oct 9, 2024
1 parent 4cd9987 commit 4d197ec
Show file tree
Hide file tree
Showing 15 changed files with 212 additions and 24 deletions.
Binary file modified public/planet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/portal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/components/CharacterCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Character } from 'rickmortyapi';
import MiniCard from './MiniCard';

export default function CharacterCard({ character }: { character: Character }) {
return (
<MiniCard title={character.name} image={character.image} description={character.species} />
);
}
10 changes: 0 additions & 10 deletions src/components/CharacterDetail.tsx

This file was deleted.

40 changes: 40 additions & 0 deletions src/components/DetailsLayout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="flex flex-col md:flex-row gap-4 p-4">
<div className="flex flex-col md:flex-row bg-white dark:bg-gray-800 shadow-lg rounded-lg">
<div className="p-4 flex-1">
<img className="mx-auto" src={image} alt={title} />
<h1 className="text-2xl font-bold mb-4 dark:text-white">{title}</h1>

<dl className="space-y-2">
{facts.map((fact, index) => (
<div key={index} className="flex justify-between items-center hover:bg-gray-200 dark:hover:bg-gray-700 p-2 rounded">
<dt className="font-semibold dark:text-gray-300">{fact.type}</dt>
<dd className="dark:text-gray-300">
{fact.url ? (
<Link to={fact.url} className="text-blue-500 hover:underline dark:text-blue-400">{fact.value}</Link>
) : fact.value}
</dd>
</div>
))}
</dl>
</div>
</div>
<div className="flex-1">
<h2 className="text-xl text-left font-semibold mb-4">{childrenTitle}:</h2>
<div className='grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'>
{children}
</div>
</div>
</section>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<MiniCard title={place.name} image={place.type === "Planet" ? PlanetImage : PortalImage} description={`${place.type}`} />
Expand Down
9 changes: 9 additions & 0 deletions src/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
12 changes: 6 additions & 6 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand All @@ -30,7 +30,7 @@ const router = createBrowserRouter([
},
{
path: '/characters/:characterId',
element: <CharacterDetail />,
element: <CharacterDetails />,
loader: characterDetailLoader
},
{
Expand All @@ -40,7 +40,7 @@ const router = createBrowserRouter([
},
{
path: '/locations/:locationId',
element: <LocationDetail />,
element: <LocationDetails />,
loader: locationDetailLoader
},
{
Expand All @@ -50,7 +50,7 @@ const router = createBrowserRouter([
},
{
path: '/episodes/:episodeId',
element: <EpisodeDetail />,
element: <EpisodeDetails />,
loader: episodeDetailLoader
},
]
Expand Down
46 changes: 46 additions & 0 deletions src/routes/CharacterDetails.tsx
Original file line number Diff line number Diff line change
@@ -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<Episode[]>([]);
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 (
<DetailsLayout title={char.name} image={char.image} facts={facts} childrenTitle={"Episodes"}>
{episodes.length > 0 ? episodes.map((episode, index) => (
<Link to={`/episodes/${episode.id}`} key={index}>
<EpisodeCard episode={episode} />
</Link>
)) : "Loading..."}
</DetailsLayout>
);
}
4 changes: 2 additions & 2 deletions src/routes/Characters.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -14,7 +14,7 @@ export default function Characters() {
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4'>
{characters.map((character) => (
<Link key={character.id} to={`/characters/${character.id}`}>
<CharacterDetail key={character.id} character={character} />
<CharacterCard key={character.id} character={character} />
</Link>
))}
</div>
Expand Down
52 changes: 52 additions & 0 deletions src/routes/EpisodeDetails.tsx
Original file line number Diff line number Diff line change
@@ -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<Character[]>([]);

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 (
<DetailsLayout title={episode.name} image={image} facts={DetailFacts} childrenTitle={"Characters"}>
{characters.length > 0 ? characters.map((character, index) => (
<Link to={`/characters/${character.id}`} key={index}>
<CharacterCard character={character} />
</Link>
)) : "Loading..."}
</DetailsLayout>
)
}
4 changes: 2 additions & 2 deletions src/routes/Episodes.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -14,7 +14,7 @@ export default function Episodes() {
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4'>
{episodes.map((episode) => (
<Link key={episode.id} to={`/episodes/${episode.id}`}>
<EpisodeDetail key={episode.id} episode={episode} />
<EpisodeCard key={episode.id} episode={episode} />
</Link>
))}
</div>
Expand Down
43 changes: 43 additions & 0 deletions src/routes/LocationDetails.tsx
Original file line number Diff line number Diff line change
@@ -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<Character[]>([]);

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 (
<DetailsLayout title={location.name} image={location.type === "Planet" ? PlanetImage : PortalImage} facts={facts} childrenTitle="Residents">
{characters.length > 0 ? characters.map((character, index) => (
<Link to={`/characters/${character.id}`} key={index}>
<CharacterCard character={character} />
</Link>
)) : "Loading..."}
</DetailsLayout>
);
}
4 changes: 2 additions & 2 deletions src/routes/Locations.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -14,7 +14,7 @@ export default function Locations() {
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4'>
{locations.map((location) => (
<Link key={location.id} to={`/locations/${location.id}`}>
<LocationDetail key={location.id} location={location} />
<LocationCard key={location.id} location={location} />
</Link>
))}
</div>
Expand Down

0 comments on commit 4d197ec

Please sign in to comment.