Proyectos

02 / 05Caso de estudio

Izak's Photos

Sitio y portafolio fotográfico

Un estudio de fotografía de demostración, bilingüe y en línea: galería de 42 fotos con enlaces compartibles, visor accesible y reservas que se guardan en Django, servidos por un solo servicio en Railway.

Portada de Izak's Photos con un retrato en luz cálida y botones para ver la galería y reservarMosaico de dos columnas en móvil con Stillness, Newsprint, Red Motion y Vows rotuladas
Mi papel
Evolución full-stack; interfaz, experiencia bilingüe, API, rendimiento y despliegue
Stack
React 18 · Vite 8 · React Router 7 · Django 5.2 · Django REST Framework · Railway
Estado
En línea en Railway · estudio de demostración

Alcance

  • −74 %de peso en las miniaturas de la galería
  • 42fotos en 4 series, cada una con su enlace
  • 2idiomas, inglés y español

El reto

Un archivo de fotografías en cuatro series tiende a volverse una cuadrícula genérica, y el mismo sitio tiene que llevar al visitante de «me gusta este trabajo» a «envío una solicitud».

Lo que construí
Reconstruí el sitio en React y Django —galería con filtros y foto abierta en la URL, visor con teclado, inglés y español, reservas con validación— y lo llevé a producción con miniaturas WebP, límite de envíos y CI.
Decisión técnica clave
Un solo servicio en producción: Django sirve el build de Vite, la API y el administrador desde el mismo origen, sin CORS.

Decisiones de diseño

  1. Portada de Izak's Photos con un retrato en luz cálida y botones para ver la galería y reservar
    Problema
    El sitio tiene que lucir el trabajo y, a la vez, llevar a reservar.
    Decisión
    Hero a sangre con dos acciones y «Reserve» fijo en la cabecera de todas las páginas.
  2. Mosaico de fotos que mezcla retrato, boda, editorial y viaje en cuatro columnas
    Problema
    Cuarenta y dos fotos en cuatro series se vuelven una cuadrícula genérica.
    Decisión
    Un mosaico que intercala las series, filtros con conteo y un visor a pantalla completa con teclado.
  3. Visor a pantalla completa con la foto Red Motion, su título y el contador 02 de 42
    Problema
    No había forma de compartir ni de enlazar una foto concreta.
    Decisión
    La categoría y la foto abierta viven en la URL; cada foto tiene su enlace.
  4. Mosaico de dos columnas en móvil con Stillness, Newsprint, Red Motion y Vows rotuladas
    Problema
    En el teléfono los títulos no se veían nunca, porque sólo aparecían al pasar el ratón.
    Decisión
    En pantallas táctiles, el lugar y el título quedan siempre visibles sobre cada foto.
  5. Serie de viajes con fotos verticales y horizontales de estrellas, calles, flores y paisajes
    Problema
    La galería descargaba 9,3 MB de JPEG completos para mostrar miniaturas pequeñas.
    Decisión
    Miniaturas WebP de 720 px en las cuadrículas; el JPEG completo sólo se carga en el visor.
  6. Confirmación de la reserva en móvil con el resumen de la sesión y el mensaje de guardado
    Problema
    En el teléfono, la confirmación de la reserva quedaba fuera de la pantalla.
    Decisión
    Tras enviar, la página baja hasta la confirmación, que resume lo enviado.
  7. Formulario con el campo de correo marcado en rojo y el mensaje Enter a valid email address
    Problema
    Un error genérico obliga a adivinar qué dato falló y a volver a escribirlo todo.
    Decisión
    La API devuelve el error de cada campo y el formulario lo marca en su sitio, sin borrar lo escrito.
  8. Portada en español con el titular Retratos en luz tranquila y el selector de idioma en ES
    Problema
    Un estudio que trabaja dentro y fuera del país no puede hablar un solo idioma.
    Decisión
    Inglés y español en todo el sitio con un clic; el idioma se recuerda y cambia el lang de la página.

Sistema

Elige un módulo: se enciende su ruta.Elige un módulo: debajo, su decisión.

Arquitectura17 módulos · 19 conexiones

Hero a sangre con botón de pausa y «Reserve» fijo en la cabecera; las fotos destacadas abren esa imagen en el visor.

React 18 · React Router 7

El mosaico intercala las series y cada foto reserva su proporción; la categoría vive en la URL.

useSearchParams · CSS propio

Teclado, deslizamiento y foco dentro del diálogo; la foto abierta es un enlace y su miniatura la cubre mientras llega el JPEG.

React 18 · useSearchParams

En táctil, el lugar y el título de cada foto siempre visibles: no hay hover que los descubra.

@media (hover: none) · CSS propio

Las fotos se rotulan como obra del portafolio y no como retrato de nadie.

React 18

Errores por campo que devuelve la API y, tras enviar, un resumen al que la página baja en el teléfono.

fetch · aria-invalid

Un Context propio con t({ en, es }), sin librería de traducción; el idioma se recuerda y cambia el lang de la página.

React Context · localStorage

Valida y guarda cada solicitud; diez por hora y cliente, y un campo trampa que acepta a los bots sin guardarlos.

Django REST Framework 3 · Throttling de DRF

Filtros, búsqueda y dos acciones: marcar solicitudes como atendidas o reabrirlas.

Django Admin

Railway sólo publica la versión nueva si /api/health/ responde.

Django 5.2

SQLite en local y PostgreSQL con DATABASE_URL, sin tocar código; un solo modelo de diez campos para las solicitudes.

SQLite · PostgreSQL · dj-database-url

Van en el build: el JPEG completo para el visor y el hero, y miniaturas WebP de 720 px para las cuadrículas.

import.meta.glob · WebP

Sólo guarda el idioma elegido: el sitio no tiene cuentas ni cookies de seguimiento.

localStorage

Railpack compila todo, migra antes de publicar y reinicia el servicio si el healthcheck falla.

Railway · Railpack

Un solo proceso sirve el build de Vite, la API y el admin desde el mismo origen: sin CORS en producción.

Gunicorn 23 · WhiteNoise 6.12

En cada push y pull request: migraciones pendientes, las 8 pruebas y el build del frontend.

GitHub Actions

Fraunces y DM Sans: la única dependencia externa en tiempo de ejecución.

Google Fonts

Inspector17 decisiones

Cliente

Portada

Hero a sangre con botón de pausa y «Reserve» fijo en la cabecera; las fotos destacadas abren esa imagen en el visor.

Tecnologías

38 herramientas en 10 áreas. En el sistema, cada módulo dice las suyas.

Lenguajes

  • JavaScript (JSX)
  • Python 3
  • CSS
  • HTML

Frontend

  • React 18.3
  • React Router 7.18
  • Vite 8.3
  • @vitejs/plugin-react 6.1

Interfaz

  • CSS propio con variables
  • Fraunces y DM Sans
  • lucide-react 0.468
  • IntersectionObserver

Estado e idioma

  • React Context
  • useSearchParams
  • localStorage
  • fetch

Backend

  • Django 5.2
  • Django REST Framework 3
  • Django Admin
  • django-cors-headers 4
  • python-dotenv 1

Datos

  • SQLite
  • PostgreSQL
  • dj-database-url 2
  • psycopg2 2.9

Imágenes

  • Miniaturas WebP de 720 px
  • Pillow (script de miniaturas)
  • import.meta.glob de Vite

Servidor y despliegue

  • Gunicorn 23
  • WhiteNoise 6.12
  • Railway
  • Railpack

Calidad y CI

  • TestCase de Django
  • GitHub Actions
  • EditorConfig

Herramientas

  • Node.js 24
  • npm
  • Git

Resultados verificables

  • En línea en izaksphotos.jonasjavier.dev como un solo servicio en Railway, con migraciones antes de publicar y healthcheck
  • Galería de 42 fotos en 4 series con filtros y la foto abierta en la URL, así cada imagen tiene su enlace
  • Visor a pantalla completa con teclado, deslizamiento táctil y el foco dentro del diálogo
  • Interfaz completa en inglés y español, con el idioma recordado
  • Miniaturas WebP que bajan la galería completa en móvil de 9,3 MB a 2,4 MB
  • Reservas guardadas en Django con errores por campo, límite de envíos y campo trampa; 8 pruebas en CI

Recorrido por módulos

44 pantallas en 7 módulos.

Portada y navegación7 pantallas

  • Portada de Izak's Photos con un retrato en luz cálida y botones para ver la galería y reservar

    La foto manda y las dos acciones principales se ven desde el primer vistazo.

  • Foto de boda del mosaico destacado con la etiqueta First Dance al pasar el ratón

    Cada foto destacada muestra su serie y abre esa imagen en el visor.

  • Tres tarjetas de servicios, Portrait Session, Brand Editorial y Wedding & Events, con precios de muestra

    Tres paquetes con duración, qué incluyen y precio; los precios son de muestra.

  • Sección The Process con cuatro pasos: Connect, Plan, Create y Deliver

    Cuatro pasos explican el trabajo de la primera idea a la entrega.

  • Tres tarjetas de testimonios de muestra con cinco estrellas, cita, nombre y tipo de sesión

    Un bloque de testimonios listo para los reales; los de ahora son de muestra.

  • Llamada final con el botón Start Booking y pie con navegación y enlace para enviar una solicitud

    El pie lleva al formulario en lugar de a un correo que no existe.

  • Página 404 con el titular This frame didn't make the final edit y botones a la galería y al inicio

    Una URL rota tiene su propia página, con salida a la galería.

Galería y visor6 pantallas

  • Galería con los filtros All, Portraits, Editorial, Weddings y Travel y el número de fotos de cada uno

    Los filtros dicen cuántas fotos hay en cada serie.

  • Mosaico de fotos que mezcla retrato, boda, editorial y viaje en cuatro columnas

    «Todo» intercala las series para que el mosaico no se agrupe por tipo.

  • Serie de bodas con nueve fotos y el filtro Weddings activo

    Cada serie tiene su propia URL, lista para compartir.

  • Serie de viajes con fotos verticales y horizontales de estrellas, calles, flores y paisajes

    Verticales y horizontales conviven porque cada foto reserva su proporción.

  • Visor a pantalla completa con la foto Red Motion, su título y el contador 02 de 42

    Cada foto tiene un enlace propio que abre directamente el visor.

  • Visor con la foto horizontal The Arch de una pareja bajo un arco de flores

    El visor adapta el marco a fotos verticales y horizontales.

Sobre mí2 pantallas

  • Página Sobre mí con un retrato rotulado From the portfolio y el titular sobre fotografía

    Sobre mí abre con una foto del portafolio, rotulada como tal.

  • Texto de The Studio junto a una tarjeta con la cita Less posing More presence y un retrato

    La historia y una cita resumen la filosofía: menos pose, más presencia.

Reservas5 pantallas

  • Página de reservas con la elección de paquete, el formulario y el panel de confirmación

    Paquete, formulario y resumen conviven en una sola vista.

  • Formulario lleno con Brand Editorial y un resumen con precio, fecha, lugar y correo

    El resumen se actualiza mientras el visitante escribe.

  • Panel de confirmación You're all set con el resumen de la solicitud guardada

    Tras enviar, la confirmación resume exactamente lo que se envió.

  • Formulario con el campo de correo marcado en rojo y el mensaje Enter a valid email address

    Si la API rechaza un dato, se marca el campo exacto y se explica.

  • Mensaje de error Couldn't reach the server bajo el botón, con el formulario aún lleno

    Si falla la red, el aviso lo dice y los datos siguen escritos.

En español3 pantallas

  • Portada en español con el titular Retratos en luz tranquila y el selector de idioma en ES

    Todo el sitio existe en inglés y en español, y recuerda la elección.

  • Galería en español con los filtros Todo, Retratos, Editorial, Bodas y Viajes

    Filtros y contadores, también en español.

  • Página de reservas en español con los paquetes y el formulario traducidos

    El formulario de reserva, traducido de principio a fin.

Administración2 pantallas

  • Lista de solicitudes en Django Admin con nombre, correo, tipo, fecha preferida, atendida y creación

    Cada solicitud llega al administrador, donde se filtra y se busca.

  • Aviso de dos solicitudes marcadas como atendidas y la lista filtrada con las cuatro pendientes

    Una acción marca varias solicitudes como atendidas de una vez.

En el teléfono19 pantallas

  • Mosaico de dos columnas en móvil con Stillness, Newsprint, Red Motion y Vows rotuladas

    En táctil cada foto lleva su lugar y su título a la vista.

  • Serie de bodas en móvil con fotos rotuladas First Dance, Just Married y The Arch

    La serie de bodas en el teléfono, con cada foto rotulada.

  • Visor en móvil con la foto Red Motion y botones de anterior y siguiente abajo

    Se pasa de foto con las flechas o deslizando.

  • Visor en móvil con la foto horizontal Sunlit Walk de una pareja caminando

    Las fotos horizontales se centran a todo el ancho.

  • Galería en móvil con los filtros por serie en dos filas y el contador de fotos

    Los filtros caben en dos filas sin esconder ninguna serie.

  • Portada de Izak's Photos en un teléfono con el titular y dos botones a todo el ancho

    Las acciones van a todo el ancho y «Reserve» nunca desaparece.

  • Menú móvil con tres enlaces, el botón Reserve y el selector de idioma

    El menú reúne navegación, reserva e idioma; se cierra con Escape.

  • Foto destacada Red Motion en móvil con su título visible sobre la imagen

    Los títulos se ven siempre, sin depender del ratón.

  • Tarjeta del paquete Portrait Session en móvil con duración, detalles y precio

    Cada paquete se lee completo en una tarjeta.

  • Pasos Connect, Plan y Create apilados en una pantalla de teléfono

    Los pasos se apilan con números grandes.

  • Sobre mí en móvil con el retrato Winter Gaze arriba y el titular debajo

    La foto va primero, recortada a 4:5 para no ocupar toda la pantalla.

  • Texto de The Studio en móvil con la ubicación en Santo Domingo

    El relato se lee de corrido en una columna.

  • Tarjeta con la cita Less posing More presence y la firma Izak en móvil

    La cita y la firma cierran la historia.

  • Página de reservas en móvil con el titular y los tres paquetes con precio

    Primero se elige el paquete y después se llena el formulario.

  • Tres paquetes y la nota de martes a sábado con anticipo del 30 por ciento en móvil

    Una nota aclara los días de sesión y el anticipo antes del formulario.

  • Formulario de reserva lleno en móvil con nombre, correo, teléfono, tipo, fecha y lugar

    El formulario va a una columna con campos amplios.

  • Confirmación de la reserva en móvil con el resumen de la sesión y el mensaje de guardado

    Tras enviar, la página baja sola hasta la confirmación.

  • Campo de correo marcado con error en el formulario de reserva en móvil

    El mismo aviso por campo, en el teléfono.

  • Portada en español en un teléfono con los botones Ver la Galería y Reservar Sesión

    La versión en español mantiene la jerarquía en el teléfono.

7 min de lectura

El caso completo

Contexto

Izak's Photos es el sitio de un estudio de fotografía de retrato, bodas, editorial y viajes con base en Santo Domingo. El estudio es ficticio: Izak no existe, y los precios, las cifras del estudio y los testimonios son contenido de muestra para enseñar el producto. Lo que sí es real es el software: está en línea, guarda solicitudes y se despliega como cualquier sitio de un cliente.

El proyecto nació en mayo de 2024 en un repositorio que creó Job Nacor y en el que colaboramos: un sitio en React y Bootstrap con siete páginas y un formulario de contacto de cuatro campos. En junio de 2026 lo reconstruí desde cero y en septiembre lo llevé a producción. Hoy mantengo esta edición y el repositorio conserva la autoría de los dos.

Problema

Un archivo amplio de fotografías tiende a convertirse en una cuadrícula genérica, y un sitio de fotógrafo tiene dos trabajos a la vez: lucir la obra y convertir la visita en una solicitud. Además, una foto que no se puede enlazar no se puede compartir, y en el teléfono todo lo que depende del ratón desaparece.

Mi función

La reconstrucción de 2026 es mía: la interfaz responsive, la experiencia bilingüe, la galería y el visor, la API de reservas, el administrador, el rendimiento de las imágenes, el despliegue en Railway, las pruebas, la integración continua y la documentación.

Restricciones

  • Un solo servicio. Django sirve la API, el administrador y el build de Vite desde el mismo dominio.
  • Sin subida de fotos. Las imágenes son archivos del build: añadir una requiere un despliegue.
  • Sin correo saliente. Las solicitudes quedan en la base de datos y se atienden desde el administrador.
  • Bilingüe escrito a mano. 256 cadenas en español junto a su versión en inglés, sin librería de traducción.

Cómo lo construí

La versión de 2026 pasó de 25 dependencias de ejecución a 6 y de siete secciones a cuatro páginas —portada, galería, sobre mí y reservas— más una 404 propia. El modelo de contacto de cuatro campos se convirtió en una solicitud de reserva con diez, y el formulario habla con la API en camelCase mientras el serializador lo traduce a los campos del modelo.

En septiembre lo revisé en cinco anchos (1440, 1024, 768, 390 y 360 px) y en los dos idiomas, y corregí lo que esa revisión sacó a la luz antes de desplegarlo: enlaces compartibles, visor accesible, miniaturas WebP, límite de envíos, página 404 y una configuración de producción más estricta.

Decisiones de arquitectura

  • Mismo origen en producción. Django y WhiteNoise sirven el build con nombres con hash y compresión, y una ruta final devuelve index.html para las rutas de React. El formulario llama a /api/contact/, así que no hace falta CORS. En desarrollo, Vite y Django corren por separado.
  • El estado de la galería en la URL. ?category= y ?photo=: una serie o una foto abierta se pueden enlazar, y las destacadas de la portada abren directamente la suya.
  • Traducción propia y mínima. Un Context con t({ en, es }) que detecta el idioma del navegador, lo recuerda y actualiza <html lang>.
  • Base de datos por configuración. SQLite en local y PostgreSQL si existe DATABASE_URL, sin cambiar código.
  • Publicar sólo lo que funciona. Railway migra antes de publicar y comprueba /api/health/ antes de dar tráfico a la versión nueva.

Rendimiento de las imágenes

La galería descargaba los JPEG completos para pintar miniaturas pequeñas: 9.303 KB en un teléfono. Un script genera ahora miniaturas WebP de 720 px, que pesan un 74 % menos que los JPEG (8.986 KB frente a 2.372 KB); el JPEG completo sólo se pide al abrir el visor. Recorrer la galería entera en un teléfono pasó de 9,3 MB a 2,4 MB. Cada foto reserva su proporción con su ancho y alto reales, y la página no salta al cargar.

Accesibilidad y seguridad

  • El carrusel de la portada tiene botón de pausa, se detiene al recibir el foco y no arranca solo con prefers-reduced-motion.
  • El visor mantiene el foco dentro y lo devuelve a la miniatura al cerrarse; se recorre con las flechas o deslizando.
  • Hay enlace para saltar al contenido, anillo de foco visible y un título de pestaña por página.
  • La API de reservas admite diez solicitudes por hora y cliente, y un campo trampa acepta los envíos de bots sin guardarlos.
  • Fuera de desarrollo, las cookies son seguras y la API navegable de DRF queda apagada.

Desafíos

La revisión en varios anchos encontró defectos que a simple vista pasaban:

  • Un marco irregular en cada miniatura. El botón que envuelve cada foto conservaba el relleno del navegador.
  • Una cabecera que nunca desenfocaba el fondo. Con las dos formas de backdrop-filter escritas, el minificador de Vite 8 sólo dejaba la versión con prefijo.
  • Un visor de 0×0 mientras cargaba. width y height no reservan espacio si el CSS los pisa con auto.
  • Un selector de idioma flotante que tapaba fotos, paquetes y la confirmación de la reserva; ahora vive en la cabecera y en el menú.
  • Una fecha en UTC en el título de cada solicitud del administrador, que no coincidía con la hora local de su creación.

Diseño y UX

Fondo casi negro y dorado apagado para que las fotos lleven el color, una serif editorial (Fraunces) para los titulares y una sans (DM Sans) para el texto. La reserva cabe en una vista: se elige un paquete, se llena el formulario y un resumen se actualiza mientras se escribe. Si la red falla, el aviso lo dice y los datos siguen escritos; si la API rechaza un dato, se marca el campo exacto.

Lo que no tiene

Para que nadie lo dé por supuesto: no hay pagos, ni cuentas de cliente, ni galerías privadas, ni subida de fotos desde el administrador, ni correo saliente: las solicitudes sólo aparecen en /admin/. Tampoco hay pruebas del frontend ni analítica.

Sobre el contenido y las capturas

El estudio, sus precios, sus cifras y sus testimonios son de muestra, y el formulario de producción guarda solicitudes de demostración: no lo uses con datos reales. Las fotografías y las personas que aparecen en ellas tienen derechos aparte de los del código, y el sitio no las atribuye a un autor real. Las capturas se hicieron el 25 de septiembre de 2026 sobre el build de producción servido en local, con solicitudes inventadas en el administrador. Las pantallas con algún defecto visible se dejaron fuera.

Aprendizajes

Las capturas automáticas encontraron tres defectos que la revisión a ojo no vio. Y los fallos más difíciles no estaban en la lógica: un reset de botones incompleto, un minificador que descartaba una propiedad y un tamaño que el CSS anulaba. Revisar el sitio en varios anchos, en los dos idiomas y con la red retenida fue lo que los sacó a la luz.

Estado actual

En línea en izaksphotos.jonasjavier.dev como un solo servicio en Railway, con el repositorio público bajo licencia GPL-3.0, la integración continua en verde y 8 pruebas del backend que pasan. Es un estudio de demostración: no tiene clientes ni reservas reales, así que no publico cifras de uso.