Ir al contenido

Taller de HTML, CSS, Bootstrap y TypeScript: galería dinámica desde un Gist

  • Modalidad: Mixta: durante la clase y por fuera de clase.
  • Formato: Individual.
  • Entregable: Repositorio individual suministrado por el equipo docente (a través de Classroom 50) con la página funcionando y el enlace al Gist en el README.md.

Al finalizar este taller, cada estudiante debe ser capaz de:

  1. Configurar un proyecto de TypeScript con tsc --init y reconocer, a grandes rasgos, para qué sirve el tsconfig.json.
  2. Publicar un conjunto de datos como JSON en un Gist de GitHub y consumirlo desde el navegador con fetch.
  3. Describir la forma de esos datos con una interface de TypeScript y usarla para tipar la respuesta.
  4. Seleccionar un elemento raíz del documento, modificarlo y renderizar contenido dentro de él usando el API del DOM.
  5. Maquetar una galería de tarjetas responsive con la grilla de Bootstrap, definiendo cuántas columnas se ven en cada breakpoint.
  6. Compilar el proyecto con tsc y verificar el resultado en el navegador con Live Server.

Van a hacer una página que no tiene sus datos escritos en el HTML. El HTML solo aporta un contenedor vacío; los datos viven en un Gist de GitHub (un archivo JSON publicado en internet), y un archivo de TypeScript se encarga de traerlos, recorrerlos y pintar una tarjeta por cada elemento.

flowchart LR
    A["Gist en GitHub<br/>(datos.json)"] -->|"fetch()"| B["script.ts<br/>(compilado a script.js)"]
    B -->|"API del DOM"| C["index.html<br/>(elemento raíz #app)"]
    C -->|"Live Server"| D["Navegador<br/>(galería de tarjetas)"]

La idea de fondo es la separación entre datos y presentación: si mañana cambian los datos del Gist, la página muestra información distinta sin tocar una sola línea de HTML ni de TypeScript.

El compilador de TypeScript y el tsconfig.json

Sección titulada «El compilador de TypeScript y el tsconfig.json»

El navegador no entiende TypeScript: solo entiende JavaScript. Por eso el flujo siempre es el mismo: ustedes escriben su código en typescript script.ts, ejecutan el compilador tsc, y este genera un archivo de javascript script.js que es el que finalmente se carga en el index.html.

El archivo tsconfig.json es el que le dice a tsc cómo debe compilar: qué versión de JavaScript generar, qué tipos existen dentro del proyecto, qué tan estricto es el chequeo de tipos, y dónde y con qué archivos deja el resultado. Se crea con tsc --init y, en el Paso 3, les damos el comando con los valores que necesita este taller.

A continuación, se explican las dos configuraciones más importantes que deben estar en el tsconfig.json para este taller:

  • La lista de tipos disponibles (opción lib) debe incluir "dom". Esa es la que hace que en TypeScript existan document, HTMLElement, fetch y otros elementos que usa el navegador; sin ella, document.getElementById(...) ni siquiera compila.
  • El formato del archivo generado: TypeScript puede producir el .js en varios formatos distintos, y no todos los entiende el navegador por sí solo. La configuración de este taller hace que tsc genere un archivo sencillo, que se puede cargar directamente con una etiqueta <script src="script.js"> en el index.html y cuyo código se ejecuta apenas el navegador lo lee. Los otros formatos son pensados para proyectos que reparten el código entre muchos archivos. Por ejemplo, en frameworks como Angular que veremos más adelante.

También conviene saber que el .js se genera junto al .ts, es decir, en la misma carpeta del index.html, que es donde este lo va a buscar.

Un Gist es un mini repositorio de GitHub pensado para compartir uno o pocos archivos sueltos. Cada Gist público expone sus archivos en una URL “cruda” (raw) con esta forma:

https://gist.githubusercontent.com/<usuario>/<id-del-gist>/raw/<nombre-del-archivo>

Esa URL devuelve el contenido del archivo tal cual, sin la interfaz de GitHub alrededor, y con los permisos necesarios para que el navegador pueda leerla desde otra página. Para el ejercicio, eso lo convierte en un servidor de datos gratuito: es la forma más simple de tener un JSON publicado en internet sin montar un backend.

De JSON a objetos tipados: fetch, await e interface

Sección titulada «De JSON a objetos tipados: fetch, await e interface»

fetch(url) hace una petición HTTP y retorna una promesa: un objeto que representa un resultado que todavía no ha llegado. Con await le decimos a JavaScript “espera a que esa promesa se resuelva antes de seguir”. Como await solo puede usarse dentro de una función marcada como async, el patrón queda así:

async function obtenerDatos(): Promise<Producto[]> {
const respuesta = await fetch(URL_DATOS); // espera la respuesta HTTP
const datos = await respuesta.json(); // espera a convertir el cuerpo en objetos
return datos;
}

El problema es que respuesta.json() retorna any: TypeScript no tiene forma de saber qué trae ese JSON. Ahí entra la interface, que es la manera de describir la forma de un objeto en TypeScript:

interface Producto {
id: number;
nombre: string;
precio: number;
}

Con la interface definida, se usa el operador as para afirmarle al compilador de qué tipo son los datos recibidos:

const datos = (await respuesta.json()) as Producto[];

A partir de ese momento el editor les autocompleta datos[0].nombre y les marca un error si escriben datos[0].nombrre. Por eso la interface debe tener exactamente los mismos atributos, con los mismos nombres y tipos, que los objetos del Gist: es el contrato entre los datos publicados y el código que los consume.

Manipular el DOM: elemento raíz, selección y renderizado

Sección titulada «Manipular el DOM: elemento raíz, selección y renderizado»

Toda la teoría del DOM está en Manipular el HTML desde TS. Aquí solo recordamos lo que van a usar y cómo se articula.

En vez de escribir las tarjetas a mano en el HTML, se deja un solo contenedor vacío identificado con un id:

<div id="app"></div>

Ese div es el elemento raíz de la aplicación: el punto del árbol DOM del que cuelga todo lo que se pinta desde TypeScript. Es exactamente el mismo patrón que usan frameworks como Angular o React. Trabajar así tiene dos ventajas: el HTML queda estable y mínimo, y todo el contenido variable se genera en un solo lugar del código.

Para obtener una referencia a ese elemento se usa el API del DOM sobre la variable global document:

Función Qué recibe Qué retorna
document.getElementById("app") El valor del atributo id, sin # El elemento con ese id, o null
document.querySelector("#app") Un selector de CSS (#app, .card, h1) El primer elemento que coincide, o null
document.querySelectorAll(".card") Un selector de CSS Una lista con todos los elementos que coinciden

Los selectores son los mismos que ya usaron en CSS (# para id, . para clase, el nombre solo para la etiqueta); pueden repasarlos en Aplicar los estilos, Selectores.

Como ambas funciones pueden retornar null (si no existe ningún elemento con ese id), en modo estricto TypeScript no deja usar el resultado directamente. La forma más corta de resolverlo es el operador ! al final de la llamada, que le afirma al compilador que ahí sí va a haber un elemento:

const raiz: HTMLElement = document.getElementById("app")!;

Con la referencia en la mano, estas son las propiedades que van a usar:

Propiedad Qué hace
raiz.innerText = "..." Cambia el texto del elemento. Lo que se asigna se muestra tal cual, aunque contenga etiquetas.
raiz.innerHTML = "<p>Hola</p>" Reemplaza todo el contenido del elemento interpretando el texto como HTML. Es lo que permite crear varios elementos de una sola vez.
raiz.className = "row g-4" Reemplaza la lista de clases CSS del elemento (aquí es como se le aplican las clases de la grilla de Bootstrap desde el código).
raiz.classList.add("...") Agrega una clase sin borrar las demás.

Para construir el HTML de cada tarjeta se usan los template literals de TypeScript: cadenas delimitadas por comillas invertidas (`) que pueden ocupar varias líneas e insertar valores con ${...}.

const html = `<h5 class="card-title">${producto.nombre}</h5>`;

Y para pasar de un arreglo de datos a una sola cadena de HTML, se combinan map (convierte cada objeto en un pedazo de HTML) y join (pega todos esos pedazos en un solo texto):

raiz.innerHTML = datos.map(crearTarjeta).join("");

Una página responsive es la que reacomoda su contenido según el ancho de la pantalla. Bootstrap resuelve esto con breakpoints: puntos de corte de ancho a partir de los cuales se aplica un conjunto distinto de clases. Repasen la grilla de Bootstrap para el detalle.

Para una galería de tarjetas iguales, la forma más directa son las clases row-cols-*, que se ponen en el contenedor (la fila) e indican cuántas columnas caben por fila en cada breakpoint. En este taller la galería debe verse así:

Breakpoint Ancho de pantalla Tarjetas por fila Clase
xs (celular) < 576 px 1 row-cols-1
sm ≥ 576 px 2 row-cols-sm-2
md ≥ 768 px 2 (hereda de sm)
lg ≥ 992 px 3 row-cols-lg-3
xl y xxl ≥ 1200 px 4 row-cols-xl-4

Es decir, el contenedor raíz debe terminar con exactamente estas clases:

row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-4 g-4

Donde row marca el elemento como fila de la grilla y g-4 define el espacio (gutter) entre tarjetas. Un breakpoint que no se declara hereda el valor del breakpoint anterior: por eso md muestra 2 tarjetas sin necesidad de escribir row-cols-md-2.

Cada elemento del arreglo se envuelve entonces en un <div class="col">, y adentro va la tarjeta:

<div class="col">
<div class="card h-100">
<img src="..." class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Título</h5>
<p class="card-text">Descripción</p>
</div>
</div>
</div>
  • El taller es individual: cada estudiante crea su propio repositorio desde Classroom 50 y hace su propia entrega.
  • El tema de los datos es libre (películas, canciones, recetas, videojuegos, cafés, plantas, lo que quieran), pero debe cumplir los mínimos de la sección Paso 1.
  • Los datos no pueden estar escritos en el index.html ni en el script.ts. Deben venir del Gist a través de fetch. El index.html que entreguen no debe contener ninguna tarjeta escrita a mano.
  • No editen script.js directamente. Ese archivo lo genera el compilador; cualquier cambio que le hagan a mano se pierde en la siguiente compilación. El código se escribe siempre en script.ts.
  • Hagan commits pequeños y con mensajes descriptivos a medida que avanzan, no un único commit al final.
  1. Verifiquen que tienen Node.js instalado (se asume que ya lo instalaron siguiendo el tutorial de configuración del ambiente):

    Ventana de terminal
    node --version
  2. Instalen el compilador de TypeScript de forma global y verifiquen la instalación:

    Ventana de terminal
    npm install -g typescript
    tsc --version
  3. Instalen la extensión Live Server en VSCode: abran el panel de extensiones (Ctrl + Shift + X en Windows y Linux, Cmd + Shift + X en macOS), busquen Live Server (de Ritwick Dey) e instálenla.

    Extensión Live Server en el marketplace de VSCode con el botón Install

  4. Creen el repositorio individual del taller desde Classroom 50:

    1. Inicien sesión en classroom50.org con su cuenta de GitHub. En Classroom 50 organizations, busquen la organización del curso (Ing. Software Moderna - ISIS2211) y hagan clic en Open.

    2. En My classrooms, ubiquen su sección y entren con View assignments.

    3. En la lista Assignments, busquen la tarea de este taller. A diferencia del taller de Git, esta tarea es individual, así que no aparece marcada como Group ni pide seleccionar equipo. Hagan clic en Accept assignment.

    4. Revisen que estén registrados con su usuario correcto y que el repositorio se vaya a crear con el nombre esperado (algo como ISIS2211/...-taller-ts-<su-usuario>). Confirmen con Accept assignment.

    5. Cuando aparezca Assignment accepted, abran el repositorio con Open repository. Como el taller es individual, no deben agregar colaboradores.

      Confirmación Assignment accepted con el botón Open repository

  5. Clonen el repositorio en su máquina y ábranlo en VSCode:

    Ventana de terminal
    git clone <url-del-repo>

    Trabajen directamente sobre main: en este taller no hace falta crear una rama.

Paso 1: Elegir el tema y diseñar la estructura de los datos

Sección titulada «Paso 1: Elegir el tema y diseñar la estructura de los datos»

Piensen en un tema que les guste y del que puedan listar varios elementos comparables entre sí: películas favoritas, canciones, recetas, videojuegos, equipos de fútbol, cafeterías del campus, plantas, razas de perros, etc.

Sobre ese tema deben definir un arreglo de objetos que cumpla estos mínimos:

  • Mínimo 6 objetos en el arreglo.

  • Mínimo 5 atributos por objeto, y todos los objetos deben tener exactamente los mismos atributos:

    Atributo Tipo Requisito
    id number Identificador único de cada elemento (1, 2, 3, …).
    (un título) string Texto corto: el nombre del elemento. Ustedes eligen el nombre del atributo (nombre, titulo, …).
    (una descripción) string Texto más largo, de una o dos frases.
    (un valor numérico) number Precio, año, duración, puntaje, calorías… lo que aplique al tema.
    imagen string URL de una imagen (ver abajo).
  • Pueden agregar más atributos si quieren (por ejemplo, un boolean como disponible, o una categoría de tipo string).

Para las imágenes usen Picsum, un servicio que entrega fotos de ejemplo del tamaño que se le pida. Basta con armar la URL así:

https://picsum.photos/seed/cafe1/400/250
  • seed/cafe1: la “semilla” de la imagen. Mientras la semilla no cambie, Picsum devuelve siempre la misma foto, así que usen una semilla distinta por cada elemento del arreglo (cafe1, cafe2, …). Si omiten la semilla, la foto cambia en cada recarga.
  • 400/250: ancho y alto en píxeles.
  1. Entren a gist.github.com con su cuenta de GitHub.

  2. Escriban una descripción para el Gist y nombren el archivo datos.json. La extensión importa: es la que activa el resaltado de sintaxis y les ayuda a detectar errores de formato.

    Formulario de creación del Gist con la descripción, el archivo datos.json y el contenido JSON

  3. Peguen su arreglo en formato JSON. Este es un ejemplo con el tema “cafés”; ustedes deben reemplazarlo por su propio tema y sus propios atributos:

    [
    {
    "id": 1,
    "nombre": "Espresso",
    "descripcion": "Café concentrado servido en taza pequeña, base de casi todas las demás preparaciones.",
    "precio": 4500,
    "imagen": "https://picsum.photos/seed/cafe1/400/250"
    },
    {
    "id": 2,
    "nombre": "Cappuccino",
    "descripcion": "Partes iguales de espresso, leche vaporizada y espuma de leche.",
    "precio": 7000,
    "imagen": "https://picsum.photos/seed/cafe2/400/250"
    },
    {
    "id": 3,
    "nombre": "Latte",
    "descripcion": "Espresso con bastante leche vaporizada y una capa delgada de espuma.",
    "precio": 7500,
    "imagen": "https://picsum.photos/seed/cafe3/400/250"
    },
    {
    "id": 4,
    "nombre": "Americano",
    "descripcion": "Espresso rebajado con agua caliente, de sabor más suave y mayor volumen.",
    "precio": 5000,
    "imagen": "https://picsum.photos/seed/cafe4/400/250"
    },
    {
    "id": 5,
    "nombre": "Mocha",
    "descripcion": "Combinación de espresso, chocolate y leche vaporizada.",
    "precio": 8500,
    "imagen": "https://picsum.photos/seed/cafe5/400/250"
    },
    {
    "id": 6,
    "nombre": "Cold brew",
    "descripcion": "Café extraído en frío durante varias horas, menos ácido y más dulce.",
    "precio": 9000,
    "imagen": "https://picsum.photos/seed/cafe6/400/250"
    }
    ]
  4. Hagan clic en Create public gist. Debe ser público, no secret.

  5. Ya creado el Gist, hagan clic en el botón Raw que aparece sobre el archivo y copien la URL de la barra de direcciones. Esa es la URL que van a usar en el código; debe verse parecida a:

    https://gist.githubusercontent.com/su-usuario/a1b2c3d4e5f6/raw/datos.json

    Gist ya creado mostrando el contenido y el botón Raw

  6. Peguen el enlace del Gist en el README.md del repositorio: hace parte del entregable.

Paso 3: Inicializar el proyecto de TypeScript

Sección titulada «Paso 3: Inicializar el proyecto de TypeScript»

Al terminar el taller, el repositorio debe tener esta estructura:

  • Directoriomi-repo-del-taller/
    • index.html
    • script.ts
    • script.js (generado por tsc, no se edita a mano)
    • tsconfig.json
    • README.md

Desde la terminal, dentro de la carpeta del repositorio clonado, ejecuten:

Ventana de terminal
tsc --init --target es2020 --lib es2020,dom --strict --module commonjs --moduleDetection auto --declaration false --declarationMap false --sourceMap false

Esto crea el tsconfig.json ya configurado para el taller. Ábranlo y confirmen que contiene, al menos, estas líneas:

{
"compilerOptions": {
"target": "es2020",
"lib": ["es2020", "dom"],
"strict": true,
"moduleDetection": "auto",
"declaration": false,
"declarationMap": false,
"sourceMap": false
}
}

Creen el archivo index.html y copien este contenido. Trae Bootstrap desde su CDN, un título, y el elemento raíz vacío (<div id="app">) donde se van a pintar las tarjetas:

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<title>Título página</title>
</head>
<body>
<div class="container py-4">
<h1 id="titulo" class="mb-4">Cargando...</h1>
<div id="app"></div>
</div>
<script src="script.js"></script>
</body>
</html>

Sobre este HTML:

  • Cambien el contenido de <title> por el nombre de su tema.
  • container py-4 centra el contenido y le da margen vertical.
  • <h1 id="titulo"> arranca con un texto provisional porque su contenido también se va a modificar desde TypeScript.
  • <div id="app"></div> es el elemento raíz. Está vacío a propósito: ni sus clases de grilla ni las tarjetas se escriben aquí, todo se asigna desde el código.
  • La etiqueta <script src="script.js"> va al final del body, después del div, para que cuando el script se ejecute el elemento raíz ya exista en el DOM. Si la pusieran en el <head>, getElementById("app") retornaría null.

Creen el archivo script.ts y ármenlo en este orden.

  1. Definan la interface con el mismo esquema del Gist. Este es el paso que conecta los datos con el código: la interface debe tener exactamente los mismos atributos, con los mismos nombres y los mismos tipos que los objetos que publicaron en datos.json. Si en el Gist el atributo se llama precio y es un número, aquí debe ser precio: number. Adapten los nombres a su tema:

    interface Cafe {
    id: number;
    nombre: string;
    descripcion: string;
    precio: number;
    imagen: string;
    }
  2. Guarden la URL del Gist en una constante y escriban la función que trae los datos. El tipo de retorno Promise<Cafe[]> dice “esta función entrega, en el futuro, un arreglo de Cafe”:

    const URL_DATOS = "https://gist.githubusercontent.com/su-usuario/a1b2c3d4e5f6/raw/datos.json";
    async function obtenerDatos(): Promise<Cafe[]> {
    const respuesta = await fetch(URL_DATOS);
    if (!respuesta.ok) {
    throw new Error(`No se pudieron cargar los datos (HTTP ${respuesta.status})`);
    }
    return (await respuesta.json()) as Cafe[];
    }

    La verificación de respuesta.ok es importante: si el Gist está mal escrito o es secreto, GitHub responde un 404 y fetch no lanza error por sí solo. Sin esa validación, el error aparecería mucho más adelante y sería más difícil de rastrear.

  3. Escriban la función que convierte un objeto en el HTML de una tarjeta. Recibe un elemento del arreglo y retorna un string con la columna y la tarjeta de Bootstrap:

    function crearTarjeta(cafe: Cafe): string {
    return `
    <div class="col">
    <div class="card h-100">
    <img src="${cafe.imagen}" class="card-img-top" alt="${cafe.nombre}">
    <div class="card-body">
    <h5 class="card-title">${cafe.nombre}</h5>
    <p class="card-text">${cafe.descripcion}</p>
    <span class="badge text-bg-primary">$${cafe.precio}</span>
    </div>
    </div>
    </div>`;
    }

    Noten que la tarjeta usa los cinco atributos de la interface. El alt de la imagen no es decorativo: es el texto que leen los lectores de pantalla y el que se muestra si la imagen no carga.

  4. Escriban la función principal, que es donde se manipula el DOM. Hace cuatro cosas: obtiene el elemento raíz, lo modifica asignándole las clases de la grilla, pide los datos, y los pinta:

    async function pintarGaleria(): Promise<void> {
    const titulo: HTMLElement = document.getElementById("titulo")!;
    const raiz: HTMLElement = document.getElementById("app")!;
    titulo.innerText = "Mis cafés favoritos";
    raiz.className = "row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-4 g-4";
    try {
    const datos = await obtenerDatos();
    raiz.innerHTML = datos.map(crearTarjeta).join("");
    } catch (error) {
    raiz.className = "";
    raiz.innerHTML = `<div class="alert alert-danger">${(error as Error).message}</div>`;
    }
    }

    El try/catch hace que, si algo falla, el error se vea en la página y no solo en la consola. Es la diferencia entre una página en blanco sin explicación y un mensaje que dice qué pasó.

  5. Llamen a la función al final del archivo. Sin esta línea no pasa absolutamente nada: las funciones anteriores solo quedan definidas.

    pintarGaleria();
  1. Compilen el proyecto desde la terminal, en la carpeta del repositorio:

    Ventana de terminal
    tsc

    Si no imprime nada, compiló bien y ya debe existir el archivo script.js al lado del script.ts. Si imprime errores, arréglenlos antes de seguir: mientras haya errores de tipo, el .js puede quedar desactualizado.

  2. Abran la página con Live Server: clic derecho sobre index.htmlOpen with Live Server. Se abre una pestaña del navegador en una dirección como http://127.0.0.1:5500/index.html.

    Menú contextual de VSCode con la opción Open with Live Server

  3. Verifiquen que la galería sea responsive: abran las herramientas de desarrollo del navegador (F12 en Windows y Linux, Cmd + Option + I en macOS), activen el modo de dispositivo (Ctrl + Shift + M, o Cmd + Shift + M en macOS) y vayan cambiando el ancho. Deben ver 1 tarjeta por fila en pantallas angostas, 2 a partir de 576 px, 3 a partir de 992 px y 4 a partir de 1200 px.

    Galería de tarjetas en pantalla ancha, con 4 tarjetas por fila

    Galería de tarjetas en pantalla angosta, con 1 tarjeta por fila

  4. Revisen la consola del navegador (pestaña Console de las mismas herramientas de desarrollo). No debe haber errores en rojo.

  1. Confirmen los cambios y súbanlos a main:

    Ventana de terminal
    git add .
    git commit -m "Agrega galería dinámica con datos del Gist"
    git push

    Sí, script.js se sube al repositorio: aunque sea un archivo generado, es el que necesita el index.html para funcionar cuando alguien más abra la página.

  2. Entren al repositorio en GitHub y verifiquen que en main estén el index.html, el script.ts, el script.js, el tsconfig.json y el README.md con el enlace al Gist. Si algo falta, corríjanlo y vuelvan a hacer push antes de seguir.

  3. La calificación solo se ejecuta cuando marcan la entrega con la etiqueta (tag) entrega. Cuando estén seguros de que main ya tiene la versión final, ejecuten:

    Ventana de terminal
    git fetch origin main && git tag -f entrega origin/main && git push -f origin entrega

    Pueden repetir ese comando las veces que quieran antes de la fecha límite: la opción -f mueve la etiqueta al último estado de main y vuelve a lanzar la revisión. Cuenta la última ejecución antes del cierre.

  4. En GitHub, entren a la pestaña Actions del repositorio y confirmen que la ejecución disparada por la etiqueta haya terminado. También pueden revisar en classroom50.org que la entrega haya sido registrada.

En el repositorio individual de Classroom 50, con todo el trabajo en la rama main y la etiqueta entrega apuntando al último commit:

  • README.md con su nombre, el tema elegido y el enlace al Gist público.
  • tsconfig.json generado con las opciones del Paso 3.
  • index.html con el elemento raíz <div id="app"></div> vacío y sin datos escritos a mano.
  • script.ts con la interface que refleja el esquema del Gist, la carga con fetch, y la manipulación del DOM.
  • script.js compilado y actualizado (debe corresponder al script.ts entregado).
  • La página, al abrirse con Live Server, muestra al menos 6 tarjetas con imagen, título, descripción y el valor numérico, distribuidas según los breakpoints definidos.
  • La etiqueta entrega publicada en GitHub apuntando al commit final de main (sin ella no se ejecuta la revisión y la entrega no cuenta).
Síntoma Causa más probable
La página queda en blanco y la consola dice Failed to fetch Abrieron el archivo con doble clic (file://) en vez de Live Server, o el Gist es secreto.
Las tarjetas muestran undefined Los nombres de los atributos de la interface no coinciden con los del Gist.
Los cambios del script.ts no se ven en el navegador Olvidaron ejecutar tsc, o el tsc falló con errores.
exports is not defined en la consola Hay un import o un export en script.ts. Elimínenlo.
Cannot find name 'document' al compilar Falta "dom" en la opción lib del tsconfig.json.
Object is possibly 'null' al compilar Falta el operador ! al final de getElementById(...).
Las tarjetas quedan una debajo de otra, siempre Falta la clase row o las clases row-cols-* en el elemento raíz.
Las tarjetas tienen alturas distintas y se ven desalineadas Falta la clase h-100 en el div.card.

Además:

  • Trabajen de a un paso a la vez y verifiquen en el navegador antes de seguir. Es mucho más fácil encontrar un error acumulado en 10 líneas que en 60.
  • Antes de escribir el código de las tarjetas, prueben que el fetch funciona con un simple console.log(datos) dentro de pintarGaleria.
  • Si cambian el contenido del Gist, recarguen la página forzando al navegador a pedir la versión nueva: Ctrl + Shift + R en Windows y Linux, Cmd + Shift + R en macOS.