Skip to content

IronPTSolutions/lab-react-got-episodes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

logo_ironhack_blue 7

LAB | React Game of Thrones Episodes

Introducción

Hace unos días construiste una Pokédex en React aplicando componentes, props, listas y conditional rendering. Hoy vas un paso más allá: vas a montar una mini-app para explorar los 73 capítulos de Juego de Tronos introduciendo estado (useState) y elevándolo a un componente padre (lifting state up).

El objetivo es practicar:

  • Componentes y composición (descomponer UI en piezas).
  • Props (read-only) y lifting state up.
  • useState para estado local en el componente raíz.
  • Listas con .map() y key.
  • Formularios controlados (<input> con value + onChange).
  • Conditional rendering (ternario, &&).

La aplicación tendrá:

  • Un listado con todos los capítulos.
  • Un buscador por nombre del capítulo.
  • Un filtro por temporada (1–8 o "Todas").
  • Una sección de capítulos favoritos.
  • La posibilidad de marcar capítulos como vistos.

⚠️ El proyecto que recibes es solo el esqueleto: todos los componentes están vacíos con TODOs. Tú implementas el JSX, el estado y la lógica.

Requisitos

  • Haz fork de este repo a tu cuenta de GitHub.
  • Clona el fork a tu máquina local.
  • Abre el proyecto en VS Code.
  • Instala dependencias y arranca el dev server:
npm install
npm run dev

Verás la página en http://localhost:5173 con un mensaje placeholder. Tu trabajo es construir el resto.

Entrega

Al terminar:

git add .
git commit -m "done"
git push origin main

Crea un Pull Request de tu fork hacia el repositorio original.

Estructura del proyecto

lab-react-got-episodes/
├── index.html                        # carga Bootstrap 5.3 por CDN
├── package.json
├── vite.config.js
└── src/
    ├── main.jsx                      # punto de entrada (no tocar)
    ├── App.jsx                       # componente raíz — aquí vive el estado
    ├── data.js                       # array con los 73 capítulos (no modificar)
    ├── index.css                     # estilos personalizados (puedes añadir aquí)
    └── components/
        ├── Navbar.jsx                # skeleton — implementar
        ├── Filters.jsx               # skeleton — compone SearchBar + SeasonFilter
        ├── SearchBar.jsx             # skeleton — input controlado
        ├── SeasonFilter.jsx          # skeleton — botones de temporada
        ├── EpisodeCard.jsx           # skeleton — card de un capítulo
        ├── EpisodeList.jsx           # skeleton — grid de cards
        └── FavoriteEpisodes.jsx      # skeleton — sección de favoritos

Bootstrap 5.3 ya está cargado por CDN en index.html. Tu trabajo es maquetar el contenido usando sus clases (container, row, col, card, btn, form-control, navbar, …). No necesitas instalar nada extra.

Dataset

Cada capítulo en src/data.js tiene esta forma:

{
  id: 4952,
  url: "http://www.tvmaze.com/episodes/4952/game-of-thrones-1x01-winter-is-coming",
  name: "Winter is Coming",
  season: 1,
  number: 1,
  airdate: "2011-04-17",
  airtime: "21:00",
  airstamp: "2011-04-18T01:00:00+00:00",
  runtime: 60,
  image: {
    medium: "http://static.tvmaze.com/uploads/images/medium_landscape/1/2668.jpg",
    original: "http://static.tvmaze.com/uploads/images/original_untouched/1/2668.jpg"
  },
  summary: "<p>Lord Eddard Stark, ruler of the North, is summoned to court ...</p>",
  _links: { /* ... */ }
}
  • Total: 73 capítulos, 8 temporadas.
  • summary viene como HTML (envuelto en <p>...</p>).
  • image.medium es una URL utilizable directamente en un <img src>.

Iteración 1 — Renderizar el primer capítulo

Antes de pintar los 73, empieza por uno solo para verificar que los datos llegan bien.

En src/App.jsx:

  1. Descomenta import episodes from "./data";.
  2. Borra el placeholder y dentro del <main> renderiza estáticamente el primer capítulo: su imagen (image.medium), su título (name) y su temporada (season).

💡 Recuerda: dentro de JSX las expresiones JS van entre llaves { }. Y image.medium es una propiedad anidada (episodes[0].image.medium).


Iteración 2 — Componente EpisodeCard

Renderizar un capítulo directamente en App no escala. Vamos a extraerlo a su propio componente.

Abre src/components/EpisodeCard.jsx y haz que:

  • Reciba un capítulo por props: episode.
  • Devuelva una card de Bootstrap con la imagen, el título, la temporada, el número de episodio (number), la fecha (airdate) y el resumen (summary).

Después, en App.jsx, importa EpisodeCard y úsalo para renderizar episodes[0]:

<EpisodeCard episode={episodes[0]} />

💡 Pistas:

  • Las clases de Bootstrap para una card simple: card, card-img-top, card-body, card-title, card-text.
  • El summary viene como HTML. Para renderizarlo como tal, usa dangerouslySetInnerHTML={{ __html: episode.summary }} (y entiende por qué se llama "dangerously"). Como alternativa segura, puedes mostrar solo el texto sin etiquetas con un .replace(/<[^>]+>/g, '').
  • Las props son read-only: el componente no modifica episode, solo lo pinta.

Iteración 3 — Lista completa con EpisodeList

Ahora que la card funciona, vamos a pintar los 73 capítulos. Pero no en App: vamos a crear un contenedor.

  1. Abre src/components/EpisodeList.jsx. Debe:

    • Recibir episodes por props.
    • Hacer .map() sobre episodes y renderizar un <EpisodeCard /> por cada uno.
    • Envolver el mapeo en un <section className="row g-3"> y meter cada card en un <div className="col-12 col-md-6 col-lg-4"> (o las columnas que prefieras) para que se vea como rejilla.
  2. En App.jsx, importa EpisodeList y úsalo:

<EpisodeList episodes={episodes} />

💡 Recuerda la prop especial key en cada hijo del .map(). Debe ser única entre hermanos. episode.id es perfecto. Si abres la consola y olvidas la key, React te avisará.


Iteración 4 — Buscador por nombre

Vamos a introducir el primer estado: el texto del buscador. Como queremos que el filtro afecte al listado, el estado vive en App.jsx (su padre común) y baja por props (lifting state up).

  1. En src/App.jsx:

    • Importa useState desde React.
    • Crea un estado: const [search, setSearch] = useState("");
    • Filtra los episodios antes de pasarlos a <EpisodeList />:
      const visibleEpisodes = episodes.filter((ep) =>
        ep.name.toLowerCase().includes(search.toLowerCase())
      );
    • Pasa visibleEpisodes a <EpisodeList episodes={visibleEpisodes} />.
  2. En src/components/SearchBar.jsx:

    • Recibe search y setSearch por props.
    • Renderiza un <input className="form-control"> controlado:
      <input
        type="text"
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="Buscar capítulo..."
      />
  3. En src/components/Filters.jsx:

    • Importa SearchBar.
    • Recibe search y setSearch por props y se los pasa a <SearchBar />.
  4. En App.jsx, monta <Filters search={search} setSearch={setSearch} /> encima del <EpisodeList />.

💡 Pista: input controlado = el value lo manda React, no el DOM. Por eso siempre que cambias el value, tienes que actualizar el estado con onChange.


Iteración 5 — Filtro por temporada

Mismo patrón que el buscador, pero con un valor numérico (o "all" para "Todas").

  1. En App.jsx:

    • Añade un segundo estado: const [season, setSeason] = useState("all");
    • Cambia el filtro para combinar ambos:
      const visibleEpisodes = episodes.filter((ep) => {
        const matchesName = ep.name.toLowerCase().includes(search.toLowerCase());
        const matchesSeason = season === "all" || ep.season === season;
        return matchesName && matchesSeason;
      });
  2. En src/components/SeasonFilter.jsx:

    • Recibe season y setSeason por props.
    • Renderiza un botón por temporada (1..8) + un botón "Todas".
    • Al hacer click, llama a setSeason(numero) o setSeason("all").
    • Resalta visualmente la temporada activa (clase distinta de Bootstrap, ej. btn-primary vs btn-outline-primary).
  3. En Filters.jsx, importa SeasonFilter y compón ambos hijos. Pasa también season y setSeason por props.

💡 Pistas:

  • Puedes hardcodear [1, 2, 3, 4, 5, 6, 7, 8], o derivarlo del dataset con [...new Set(episodes.map(e => e.season))].sort().
  • Cuidado con los tipos: los botones devuelven strings por defecto si usas e.target.value. Asegúrate de pasar números a setSeason para que la comparación ep.season === season funcione (o convierte con Number(...)).

Iteración 6 — Marcar como favorito

Ahora vamos a introducir acciones: el usuario hace click en un botón de una card y eso modifica el estado del padre.

  1. En App.jsx:

    • Añade un estado: const [favs, setFavs] = useState([]); (array de IDs).
    • Crea la función:
      function addFav(id) {
        if (!favs.includes(id)) {
          setFavs([...favs, id]);
        }
      }
    • Pásala a <EpisodeList /> como prop onFav junto con favs.
  2. En EpisodeList.jsx, recibe favs y onFav y pásaselos a cada <EpisodeCard />. A la card, en lugar del array completo, pásale solo si este episodio está marcado: isFav={favs.includes(episode.id)}.

  3. En EpisodeCard.jsx:

    • Recibe isFav y onFav por props.
    • Añade un botón <button className="btn btn-sm btn-outline-warning">♥ Favorito</button> que llame a onFav(episode.id).
    • Si isFav es true, cambia el estilo del botón (por ejemplo btn-warning sólido).
  4. En src/components/FavoriteEpisodes.jsx:

    • Recibe la lista de capítulos favoritos ya filtrada por props.
    • Si la lista está vacía, muestra "Aún no has marcado favoritos".
    • Si tiene elementos, lista los títulos (puedes reutilizar <EpisodeCard /> o hacer un resumen tipo S01·E01 — Winter is Coming).
  5. En App.jsx, encima del <EpisodeList />, monta:

    <FavoriteEpisodes favoriteEpisodes={episodes.filter((ep) => favs.includes(ep.id))} />

💡 Pistas:

  • Nunca mutes el array con favs.push(id). Crea uno nuevo con [...favs, id]. React detecta cambios por referencia.
  • Si quieres permitir desmarcar, conviértelo en toggleFav y usa favs.filter(favId => favId !== id) cuando ya esté.

Iteración 7 — Marcar como visto

Mismo patrón que favoritos, pero con un toggle y un efecto visual en la card.

  1. En App.jsx:

    • Añade el estado: const [watched, setWatched] = useState([]);
    • Crea:
      function toggleWatched(id) {
        if (watched.includes(id)) {
          setWatched(watched.filter((w) => w !== id));
        } else {
          setWatched([...watched, id]);
        }
      }
  2. Pasa watched y toggleWatched por el árbol igual que hiciste con favs:

    • EpisodeList recibe watched y onToggleWatched.
    • Cada EpisodeCard recibe isWatched={watched.includes(episode.id)} y onToggleWatched.
  3. En EpisodeCard.jsx:

    • Añade un botón "✓ Visto" que llame a onToggleWatched(episode.id).
    • Si isWatched es true, aplica un estilo distinto a toda la card:
      • Una clase extra, por ejemplo watched, con opacity: 0.6 en tu index.css.
      • O un badge "Visto" en la esquina con <span className="badge bg-success">Visto</span>.

💡 Reflexión: fíjate en cómo los componentes "tontos" (EpisodeCard, EpisodeList) no cambian entre la iteración 6 y la 7 más allá de recibir una prop más. Esa es la idea: separar dónde vive el estado (en el padre) de dónde se pinta (en los hijos).


Bonus

Si tienes tiempo:

  • Renderizar summary como HTML real: usa dangerouslySetInnerHTML={{ __html: episode.summary }} y entiende los riesgos (XSS si los datos vinieran del usuario; aquí son seguros porque controlamos el dataset).
  • Formatear airdate a un formato legible (new Date(episode.airdate).toLocaleDateString("es-ES")).
  • Ordenar los capítulos por airstamp ascendente o descendente.
  • Persistir favs y watched en localStorage para que sobrevivan a refrescos (useEffect + JSON.stringify / JSON.parse).
  • Modo "solo no vistos": un checkbox que filtre los capítulos que aún no has visto.

Recursos

Happy coding! ❤️

About

LAB | React Game of Thrones Episodes — state, lists & keys, lifting state up (Vite + React 18 + Bootstrap CDN)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages