Sistema de gestión de viajes y reservas - Interfaz de usuario construida con Next.js 16, React 19 y TypeScript.
El frontend utiliza un stack tecnológico moderno diseñado para desarrollo rápido, rendimiento óptimo y experiencia de usuario fluida:
Next.js 16.0.5: Framework React con App Router, Server Components, y optimizaciones automáticas de rendimiento. Elegido por su sistema de routing basado en archivos y capacidades de SSR/SSG.
React 19.2.0: Biblioteca principal de UI con Concurrent Rendering y Automatic Batching para mejorar el rendimiento de actualizaciones de estado.
TypeScript 5: Lenguaje principal del frontend. Proporciona type-safety, mejor autocompletado en IDEs y detección temprana de errores.
Tailwind CSS 3.3.3: Framework de utilidades CSS para diseño responsive rápido y consistente. Incluye configuración personalizada para los colores de marca ProacTrip.
Framer Motion 12.31.0: Biblioteca de animaciones declarativas para transiciones fluidas, gestos táctiles y elementos interactivos.
Lucide React 0.563.0: Set de iconos SVG modernos y optimizados con soporte para personalización de color y tamaño.
Lottie Animations (@dotlottie/react-player 1.6.19): Animaciones vectoriales de alta calidad para loaders, splash screens y elementos visuales complejos.
El frontend sigue la estructura de App Router de Next.js con separación clara de responsabilidades:
/app: Rutas y layouts de la aplicación usando el sistema de archivos/components: Componentes reutilizables organizados por dominio (layout, home, ui, iconos)/lib: Lógica compartida (API client, utilidades, constantes, tipos)/hooks: React hooks personalizados para lógica reutilizable/public: Assets estáticos (imágenes, animaciones Lottie, fuentes)
La aplicación utiliza Client Components ('use client') para interactividad y Server Components cuando es posible para optimizar el bundle size y mejorar el rendimiento inicial.
Para autenticación, los tokens PASETO v4 se almacenan en localStorage y se envían en cada request mediante el header Authorization: Bearer <token>. El cliente API (/app/lib/api.ts) maneja automáticamente la renovación de tokens (5 minutos antes de expirar) y redirección en caso de sesión expirada.
El proyecto está configurado con path aliases en tsconfig.json:
"paths": {
"@/*": ["./*"]
}Esto permite imports limpios desde la raíz:
import { Button } from '@/components/ui/Button';
import { getUserProfile } from '@/app/lib/api';
⚠️ Importante:@/apunta a la raíz del proyecto, no a/app. Para acceder a archivos en/app, usa@/app/...
frontend/
├── app/ # App Router de Next.js
│ ├── auth/ # Páginas de autenticación
│ │ ├── login/ # Login con email/password y Google OAuth
│ │ ├── register/ # Registro de nuevos usuarios
│ │ ├── forgot-password/ # Recuperación de contraseña
│ │ ├── reset-password/ # Reseteo de contraseña con token
│ │ ├── verify-email/ # Verificación de email
│ │ ├── resend-verification/ # Reenvío de email de verificación
│ │ └── callback/ # Callback de Google OAuth
│ ├── home/ # Dashboard principal (requiere auth)
│ │ ├── profile/ # Perfil de usuario
│ │ ├── hoteles/ # Búsqueda de hoteles (placeholder)
│ │ ├── vuelos/ # Búsqueda de vuelos (placeholder)
│ │ ├── ofertas/ # Ofertas especiales (placeholder)
│ │ ├── plan/ # Planificador de viajes (placeholder)
│ │ ├── contactanos/ # Formulario de contacto
│ │ ├── favoritos/ # Destinos y reservas favoritas (placeholder)
│ │ ├── carrito/ # Carrito de compras (placeholder)
│ │ └── mis-compras/ # Historial de reservas (placeholder)
│ ├── lib/ # Código compartido
│ │ ├── constants/ # Constantes (países, monedas, avatares, destinos)
│ │ ├── types/ # TypeScript types e interfaces
│ │ ├── utils/ # Utilidades (detección de ubicación)
│ │ └── api.ts # Cliente API con refresh automático de tokens
│ ├── layout.tsx # Layout raíz con providers y LocationDetector
│ ├── page.tsx # Splash screen de bienvenida
│ └── globals.css # Estilos globales y configuración de Tailwind
├── components/ # Componentes React reutilizables
│ ├── home/ # Componentes específicos del home
│ │ └── DestinationCard.tsx # Tarjeta de destino con animaciones
│ ├── iconos/ # Iconos personalizados
│ │ └── GoogleIcon.tsx # Logo de Google para OAuth
│ ├── layout/ # Componentes de layout
│ │ ├── Navbar.tsx # Barra de navegación principal
│ │ └── CookieBanner.tsx # Banner de consentimiento de cookies
│ ├── ui/ # Componentes UI genéricos
│ │ ├── Button.tsx # Botón reutilizable
│ │ ├── InputField.tsx # Input de texto con validación
│ │ ├── Loader.tsx # Indicador de carga animado
│ │ └── Divider.tsx # Separador visual
│ ├── LocationDetector.tsx # Detector de ubicación (usuarios anónimos)
│ └── LoginLocationDetector.tsx # Detector de ubicación (usuarios autenticados)
├── hooks/ # React hooks personalizados
│ └── useAuth.ts # Hook de autenticación y gestión de sesión
├── public/ # Assets estáticos
│ ├── animations/ # Archivos Lottie (.lottie)
│ │ ├── loader.lottie # Animación de carga
│ │ └── lupa.lottie # Animación de búsqueda
│ ├── assets/ # Imágenes de UI
│ │ └── loginRegister/ # Fondos de auth
│ └── images/ # Imágenes de contenido
│ └── destinations/ # Fotos de destinos turísticos
├── Dockerfile # Configuración de contenedor Docker
├── docker-compose.yml # Orquestación de servicios
├── next.config.ts # Configuración de Next.js
├── tailwind.config.ts # Configuración de Tailwind CSS
├── tsconfig.json # Configuración de TypeScript
├── .dockerignore # Archivos excluidos de Docker
├── .gitignore # Archivos excluidos de Git
├── .env.local # Variables de entorno (no commitear)
├── .env.example # Template de variables de entorno
└── package.json # Dependencias y scripts
Asegúrate de tener instalado:
- Node.js 20.x o superior
- npm 9.x o superior
- Docker y Docker Compose (opcional, para desarrollo en contenedor)
git clone https://github.com/ProacTrip/Frontend.git
cd frontendnpm install --legacy-peer-deps
⚠️ La flag--legacy-peer-depses necesaria debido a que algunas dependencias aún no declaran compatibilidad explícita con React 19.
Crea un archivo .env.local en la raíz del proyecto:
cp .env.example .env.localContenido de .env.local:
NEXT_PUBLIC_API_URL=http://localhost:8080npm run devEl servidor iniciará en http://localhost:3000 con hot-reload habilitado.
Asegúrate de tener Docker Desktop instalado y corriendo:
docker --version
docker compose versionCrea .env.local como se indicó arriba.
docker compose up -dLa primera ejecución tomará unos minutos para construir la imagen.
docker compose logs -f frontendDeberías ver:
✓ Ready in XXXms
○ Local: http://localhost:3000
# Ver logs en tiempo real
docker compose logs -f frontend
# Detener contenedor
docker compose down
# Reiniciar contenedor
docker compose restart frontend
# Entrar al contenedor
docker compose exec frontend sh
# Reconstruir imagen (si cambias Dockerfile)
docker compose up -d --build- Abre http://localhost:3000 en tu navegador
- Deberías ver el splash screen de ProacTrip con animación Lottie
- Después de 5 segundos, redirige automáticamente a
/auth/login
El frontend se comunica con el backend mediante un cliente API centralizado en /app/lib/api.ts.
Características principales:
- ✅ Refresh automático de tokens: Detecta tokens expirados y los renueva usando refresh tokens
- ✅ Renovación proactiva: Renueva tokens 5 minutos antes de que expiren
- ✅ Race condition prevention: Evita múltiples requests simultáneos de refresh mediante promesas compartidas
- ✅ Retry en 401: Reintenta automáticamente requests fallidos después de renovar el token
- ✅ Type-safe: Interfaces TypeScript para todos los tipos de datos
- ✅ Logout automático: Limpia sesión y redirige si el refresh token también expira
Ejemplo de uso:
import { apiFetch, getUserProfile, updateUserProfile } from '@/app/lib/api';
// GET request
const profile = await getUserProfile();
// PUT request
const updatedProfile = await updateUserProfile({
first_name: 'Juan',
last_name: 'Pérez'
});
// POST request genérico
const response = await apiFetch('/api/v1/custom-endpoint', {
method: 'POST',
body: JSON.stringify({ data: 'example' })
});Autenticación (públicos):
POST /api/v1/auth/register - Registro de usuarios
POST /api/v1/auth/login - Login con email/password
POST /api/v1/auth/refresh - Renovación de access tokens
POST /api/v1/auth/logout - Cierre de sesión
GET /api/v1/auth/verify-email - Verificación de email
POST /api/v1/auth/resend-verification - Reenvío de email de verificación
POST /api/v1/auth/forgot-password - Solicitud de reset de contraseña
POST /api/v1/auth/reset-password - Reseteo de contraseña
GET /api/v1/auth/google - Inicio de sesión con Google
GET /api/v1/auth/google/callback - Callback de Google OAuth
Usuarios (protegidos - requieren token):
GET /api/v1/user/profile - Obtener perfil completo
PUT /api/v1/user/profile - Actualizar perfil
POST /api/v1/user/avatar - Subir avatar (multipart/form-data)
GET /api/v1/user/medical - Obtener ficha médica
PUT /api/v1/user/medical - Actualizar ficha médica
Ubicación (protegidos):
POST /api/v1/user/current-location - Guardar ubicación actual (pendiente en backend)
⚠️ Nota: Algunos endpoints están pendientes de implementación en el backend. Ver sección de "Funcionalidades Pendientes" (esta mas abajo).
Register (POST /api/v1/auth/register):
{
"email": "user@example.com",
"password": "12345678",
"preferred_language": "es",
"preferred_currency": "EUR",
"timezone": "Europe/Madrid",
"location": {
"city": "Madrid",
"region": "Madrid",
"country": "ES",
"country_name": "Spain",
"latitude": 40.4165,
"longitude": -3.7026,
"postal": "28001"
}
}Current Location (POST /api/v1/user/current-location):
{
"location": {
"city": "París",
"region": "Île-de-France",
"country": "FR",
"country_name": "France",
"latitude": 48.8566,
"longitude": 2.3522,
"postal": "75001"
}
}ProacTrip implementa un sistema de detección de ubicación en dos capas usando la API externa ipapi.co:
- Proveedor: ipapi.co
- Endpoint:
https://ipapi.co/json/ - Método: GET (sin API key requerida)
- Límite: 1000 requests/día (plan gratuito)
- Documentación: https://ipapi.co/api/
Ventajas de usar API externa:
- ✅ Datos más precisos que APIs nativas del navegador
- ✅ Incluye ciudad, región, país, coordenadas
- ✅ No requiere permisos de geolocalización del usuario
- ✅ Funciona en cualquier dispositivo/navegador
Componente: /components/LocationDetector.tsx
Ubicación: Layout raíz (/app/layout.tsx)
Función: Detecta la ubicación del usuario mediante ipapi.co y la guarda en localStorage
Flujo de detección:
- Llama a
https://ipapi.co/json/(timeout: 3 segundos) - Si tiene éxito → guarda datos completos en
localStorage - Si falla → usa fallback con APIs nativas del navegador:
Intl.DateTimeFormat().resolvedOptions().timeZone→ timezonenavigator.language→ idioma- Mapeo manual
timezone → monedausando/app/lib/constants/currencies.ts
Datos detectados y guardados:
localStorage.setItem('user_location', JSON.stringify({
timezone: "Europe/Madrid",
currency: "EUR",
language: "es",
location: {
city: "Madrid",
region: "Madrid",
country: "ES",
country_name: "Spain",
latitude: 40.4165,
longitude: -3.7026,
postal: "28001"
}
}));Uso: Estos datos se utilizan para prellenar el formulario de registro con valores sensatos (idioma, moneda, zona horaria, ubicación).
Componente: /components/LoginLocationDetector.tsx
Ubicación: Layout de home (/app/home/layout.tsx)
Función: Detecta la ubicación actual y la envía al backend para recomendaciones personalizadas
Flujo:
- Usuario hace login y llega a
/home LoginLocationDetectordetecta ubicación (API externa)- Envía solo la ubicación al backend:
POST /api/v1/user/current-location
{
"location": {
"city": "Madrid",
"region": "Madrid",
"country": "ES",
"country_name": "Spain",
"latitude": 40.4165,
"longitude": -3.7026,
"postal": "28001"
}
}Diferencia clave con LocationDetector:
- LocationDetector: Guarda en
localStorage(NO envía al backend) - LoginLocationDetector: Envía al backend (para recomendaciones en tiempo real)
Uso futuro (cuando backend lo implemente):
- Recomendar hoteles/vuelos cerca de la ubicación actual
- Mostrar ofertas relevantes a la zona
- Ajustar búsquedas por proximidad
Página: /app/home/profile/page.tsx
Endpoint: PUT /api/v1/user/profile
Función: Cambio manual de preferencias que se guarda en el backend
Campos editables:
preferred_language(ej: "es", "en", "fr")preferred_currency(ej: "EUR", "USD", "GBP")timezone(ej: "Europe/Madrid")
Estos cambios son permanentes y afectan a todas las futuras sesiones del usuario.
/auth/login
- Login con email/password
- Botón de Google OAuth con ícono oficial
- Animaciones de transición con Framer Motion
- Validación de campos en tiempo real
- Redirección automática a
/homedespués del login - Link a "¿Olvidaste tu contraseña?"
/auth/register
- Registro de nuevos usuarios
- Prellenado automático de idioma/moneda/zona horaria usando
LocationDetector - Envía ubicación completa al backend (ciudad, país, coordenadas)
- Validación de contraseña con requisitos visuales
- Confirmación de contraseña
- Envío automático de email de verificación
- Redirección a
/auth/logintras registro exitoso
/auth/verify-email
- Verificación automática de email mediante token en URL
- 3 estados: loading → success → error
- Animación Lottie durante la verificación
- Redirección automática a
/hometras 2 segundos
/auth/forgot-password
- Formulario de recuperación de contraseña
- 2 vistas: formulario → confirmación de envío
- Validación de email
/auth/reset-password
- Formulario de nueva contraseña
- Validación de token en URL
- Requisitos de contraseña visibles
- Confirmación de contraseña
- Redirección a login tras éxito
/auth/callback
- Callback de Google OAuth
- Procesamiento automático de tokens
- Redirección a
/home
/home (Página Principal)
- Carrusel animado de 7 destinos turísticos
- Fondo con transiciones suaves (Framer Motion)
- Controles de navegación (flechas izquierda/derecha)
- Botón "Buscar" con animación de relleno
- Botón de favoritos con estado persistente (localStorage)
- Totalmente responsive (mobile-first)
- Auto-play del carrusel (opcional)
/home/profile
- Visualización y edición de perfil completo
- Selector de avatar con 20 emojis predefinidos
- Campos editables:
- Nombre y apellido
- Nacionalidad (selector con banderas)
- Teléfono (prefijo auto-actualizado según país)
- Email (no editable)
- Visualización de preferencias:
- Idioma preferido
- Moneda preferida
- Zona horaria
- Botón "Guardar cambios" con animación de carga
/home/contactanos
- Formulario de contacto funcional
- Diseño de 2 columnas: formulario + información de contacto
- Tarjetas informativas:
- Teléfono de contacto
- Dirección física
- Horario de atención
- Animación de envío con
Loadercomponent - Validación de campos
- Mensaje de confirmación tras envío
Páginas Placeholder (Pendientes de Backend):
Estas páginas muestran un mensaje "Próximamente..." con emoji relacionado:
/home/hoteles🏨 - Búsqueda de hoteles/home/vuelos✈️ - Búsqueda de vuelos/home/ofertas🎉 - Ofertas especiales/home/plan📋 - Planificador de viajes/home/favoritos❤️ - Favoritos guardados/home/carrito🛒 - Carrito de compras/home/mis-compras🛍️ - Historial de reservas
/ (Página de Inicio)
- Animación Lottie de bienvenida (búsqueda con lupa)
- Timer de 5 segundos con redirección automática a
/auth/login - Responsive con layout flexible
- Optimizado para carga rápida
- Fondo con gradiente de marca
/components/ui/Loader.tsx
Indicador de carga con animación Lottie profesional.
Props:
text?: string- Texto debajo de la animaciónsize?: 'sm' | 'md' | 'lg'- Tamaño (default: 'md')
/components/ui/InputField.tsx
Input de texto con validación y soporte para iconos.
<InputField
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={emailError}
icon={}
/>Props:
label: string- Etiqueta del campotype?: string- Tipo de input (default: 'text')value: string- Valor controladoonChange: (e) => void- Handler de cambioerror?: string- Mensaje de erroricon?: ReactNode- Ícono prefijodisabled?: boolean- Estado deshabilitado
/components/ui/Button.tsx
Botón reutilizable con variantes y estados.
}
>
ContinuarProps:
variant?: 'primary' | 'google'- Estilo del botónonClick?: () => void- Handler de clickisLoading?: boolean- Muestra spinnericon?: ReactNode- Ícono opcionaldisabled?: boolean- Estado deshabilitadochildren: ReactNode- Contenido del botón
/components/ui/Divider.tsx
Separador visual con texto opcional.
/components/layout/Navbar.tsx
Barra de navegación principal del dashboard.
Características:
- Links animados con efecto de subrayado progresivo
- Dropdown de perfil de usuario (avatar + nombre)
- Opciones: Ver perfil, Cerrar sesión
- Responsive con menú hamburguesa en móvil
- Logo de ProacTrip clicable (va a
/home) - Links principales:
- Plan ProacTrip
- Home
- Hoteles
- Vuelos
- Ofertas
- Contáctanos
/components/layout/CookieBanner.tsx
Banner de consentimiento de cookies (GDPR).
Características:
- Mensaje informativo sobre uso de cookies
- Botones "Aceptar" y "Rechazar"
- Persistencia en
localStorage(cookies_accepted) - Animación de entrada desde abajo
- Se oculta automáticamente tras aceptar/rechazar
/components/home/DestinationCard.tsx
Tarjeta de destino turístico con animaciones.
<DestinationCard
destination={{
id: 1,
name: "España",
place: "Barcelona",
description: "Ciudad de arte y arquitectura",
image: "/images/destinations/espana.jpg"
}}
isFavorite={false}
onToggleFavorite={() => {}}
/>Props:
destination: Destination- Objeto con datos del destinoisFavorite: boolean- Estado de favoritoonToggleFavorite: () => void- Toggle favorito
Características:
- Imagen de fondo con overlay
- Botón de favorito (corazón animado)
- Título, lugar y descripción
- Animación hover (escala + sombra)
- Totalmente responsive
Array de 65+ países con información completa.
Estructura:
interface Country {
code: string; // Código ISO-2 (ES, FR, US)
name: string; // Nombre completo
flag: string; // Emoji de bandera
phone: string; // Prefijo telefónico (+34, +33, +1)
}Helpers disponibles:
getCountryByCode('ES')
// { code: 'ES', name: 'España', flag: '🇪🇸', phone: '+34' }
getCountryByName('España')
// { code: 'ES', name: 'España', flag: '🇪🇸', phone: '+34' }Mapeo de 150+ zonas horarias a sus monedas correspondientes.
Estructura:
export const TIMEZONE_CURRENCY_MAP: Record = {
'Europe/Madrid': 'EUR',
'America/New_York': 'USD',
'Asia/Tokyo': 'JPY',
// ... 150+ más
};Helper disponible:
getCurrencyFromTimezone('Europe/Madrid') // 'EUR'
getCurrencyFromTimezone('America/New_York') // 'USD'
getCurrencyFromTimezone('Asia/Tokyo') // 'JPY'Este mapeo se usa en el fallback de LocationDetector cuando la API externa falla.
20 emojis predefinidos para avatares de usuario.
export const AVATARS = [
'😀', '😎', '🚀', '🌟', '💼',
'🎨', '🎭', '🎪', '🎯', '🎲',
// ... 10 más
];
export const DEFAULT_AVATAR = '😎';Helper disponible:
isValidAvatar('😎') // true
isValidAvatar('🦄') // false (no está en la lista)7 destinos turísticos destacados para el carrusel del home.
Estructura:
interface Destination {
id: number;
name: string; // "España"
place: string; // "Barcelona"
description: string; // "Ciudad de arte y arquitectura"
image: string; // "/images/destinations/espana.jpg"
}Destinos incluidos:
- España - Barcelona
- Francia - París
- Italia - Roma
- Grecia - Atenas
- Japón - Tokio
- Brasil - Río de Janeiro
- Estados Unidos - Nueva York
/* Colores de marca ProacTrip */
--primary: #FF6B6B; /* Rojo coral principal */
--primary-dark: #ff5252; /* Hover/Active state */
--primary-light: #ff8a80; /* Gradientes y fondos suaves */
/* Navbar */
--navbar-bg: #c54141; /* Fondo de la barra de navegación */
/* Auth pages */
--auth-primary: #8d6e63; /* Tonos marrones cálidos */
--auth-secondary: #795548; /* Variante oscura */
/* Neutrales (Tailwind) */
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
/* Estados */
--success: #10b981; /* Verde éxito */
--error: #ef4444; /* Rojo error */
--warning: #f59e0b; /* Amarillo warning */sm: 640px /* Móvil grande / Phablet */
md: 768px /* Tablet vertical */
lg: 1024px /* Desktop / Tablet horizontal */
xl: 1280px /* Desktop grande */
2xl: 1536px /* Desktop extra grande */El proyecto usa el patrón mobile-first de Tailwind:
// ✅ CORRECTO (mobile-first)
className="text-sm md:text-base lg:text-lg"
// Por defecto text-sm, tablet text-base, desktop text-lg
// ❌ INCORRECTO (desktop-first)
className="text-lg md:text-base sm:text-sm"Transiciones de página (Framer Motion):
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}Botones con hover:
whileTap={{ scale: 0.98 }}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}Efecto de relleno progresivo (botón Buscar del home):
/* Pseudo-elemento que se desliza de izquierda a derecha */
.group:hover .bg-fill {
transform: translateX(0);
transition: transform 700ms ease-out;
}
.bg-fill {
transform: translateX(-100%);
}Carrusel de destinos:
// Cambio de slide con animación
animate={{ x: -currentSlide * 100 + '%' }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}# Desarrollo local
npm run dev # Inicia servidor en localhost:3000 con hot-reload
# Producción
npm run build # Genera build optimizado para producción
npm start # Inicia servidor de producción (requiere build previo)
# Linting
npm run lint # Ejecuta ESLint en todo el proyecto# Iniciar servicios
docker compose up -d # Inicia contenedor en background
docker compose up # Inicia con logs en foreground
# Detener servicios
docker compose down # Detiene y elimina contenedores
# Logs
docker compose logs -f frontend # Ver logs en tiempo real
docker compose logs frontend # Ver logs históricos
# Reiniciar
docker compose restart frontend # Reinicia solo el servicio frontend
# Reconstruir
docker compose up -d --build # Reconstruye la imagen (tras cambios en Dockerfile)
# Acceder al contenedor
docker compose exec frontend sh # Abre shell dentro del contenedor
docker compose exec frontend npm run build # Ejecuta comando dentro del contenedor
# Limpiar todo
docker compose down -v # Detiene y elimina volúmenesConfiguración de Webpack para Docker:
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = {
fs: false, // Desactiva módulo fs en cliente
};
}
// Hot reload en Docker mediante polling
config.watchOptions = {
poll: 1000, // Revisa cambios cada segundo
aggregateTimeout: 300 // Espera 300ms antes de recargar
};
return config;
}¿Por qué polling?
Docker monta archivos mediante volúmenes, lo que puede no disparar eventos de cambio de archivo nativos. El polling fuerza a Next.js a revisar cambios periódicamente.
Path Aliases:
"paths": {
"@/*": ["./*"]
}Permite imports desde la raíz:
import { Button } from '@/components/ui/Button';
import { COUNTRIES } from '@/app/lib/constants/countries';JSX Runtime:
"jsx": "react-jsx"Usa el nuevo JSX transform de React 19 (no requiere import React).
Content Paths:
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
]Tailwind escanea estos archivos para generar solo las clases CSS usadas.
Volúmenes:
volumes:
- .:/app # Monta código fuente
- frontend_node_modules:/app/node_modules # Volumen nombrado para node_modules
volumes:
frontend_node_modules: # Crea volumen persistente gestionado por Docker¿Por qué volumen nombrado para node_modules?
Evita que node_modules de tu máquina local sobrescriba el del contenedor. Docker gestiona node_modules internamente, mejorando compatibilidad entre sistemas operativos.
# URL del backend API
NEXT_PUBLIC_API_URL=http://localhost:8080# URL del backend API
NEXT_PUBLIC_API_URL=http://localhost:8080
⚠️ Seguridad:
- Variables con prefijo
NEXT_PUBLIC_son accesibles en el cliente (browser)- NO incluyas API keys privadas, secrets o credenciales con este prefijo
.env.localestá en.gitignorey NO debe commitearse
NEXT_PUBLIC_API_URL=https://api.proactrip.comSíntoma: npm install falla con:
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^18.0.0" from ...
Causa: React 19 es relativamente nuevo y algunas dependencias aún no declaran compatibilidad explícita.
Solución:
npm install --legacy-peer-depsEsta flag le dice a npm que ignore conflictos de peer dependencies.
Síntoma: Haces cambios en el código pero el navegador no se actualiza automáticamente.
Solución 1: Verifica que next.config.ts incluye watchOptions:
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300
};Solución 2: Reinicia el contenedor:
docker compose restart frontendSolución 3: Reconstruye la imagen:
docker compose up -d --buildSíntoma: Requests fallan con error en consola:
Access to fetch at 'http://localhost:8080/api/v1/...' from origin 'http://localhost:3000'
has been blocked by CORS policy
Causa: El backend no permite requests desde http://localhost:3000.
Solución (Backend - Marco Aurelio debe hacer esto):
En el backend (Go + Echo), configurar CORS:
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Authorization", "Content-Type"},
}))Verificación:
- Backend corriendo en
http://localhost:8080 - Variable
NEXT_PUBLIC_API_URL=http://localhost:8080en.env.local - CORS habilitado en backend
Síntoma: La sesión se cierra frecuentemente sin razón aparente.
Solución:
El cliente API debería manejar esto automáticamente. Verifica en DevTools → Network:
- Busca requests a
/api/v1/auth/refresh - Deberían ejecutarse automáticamente 5 min antes de que expire el access token
- Si ves que fallan con 401, significa que el refresh token también expiró
Configuración del backend:
- Access token: 1 hora de duración
- Refresh token: 30 días de duración
- Frontend renueva access token 5 min antes de expirar
Si refresh token expira, el usuario debe volver a hacer login.
Síntoma: Placeholders o imágenes rotas en el carrusel del home.
Solución:
Verifica que existen los archivos en /public/images/destinations/:
ls public/images/destinations/Deberías ver:
espana.jpg
francia.jpg
italia.jpg
grecia.jpg
japon.jpg
brasil.jpg
estadosunidos.jpg
Si faltan, añade las imágenes correspondientes.
Síntoma: Después de hacer login exitoso, aparece error 404.
Causa posible: El backend no está corriendo o la URL es incorrecta.
Verificación:
# Verifica que el backend responde
curl http://localhost:8080/api/v1/health
# Si da error, inicia el backend
cd ../backend
go run main.goSíntoma: LocationDetector detecta ubicación cada vez que recargas la página.
Causa: Navegación privada/incógnito borra localStorage al cerrar la pestaña.
Solución: Usa el navegador en modo normal (no incógnito) para desarrollo.
Síntoma: npm run build muestra errores de TypeScript.
Solución:
# Limpia caché de Next.js
rm -rf .next
# Reinstala dependencias
rm -rf node_modules
npm install --legacy-peer-deps
# Vuelve a intentar el build
npm run buildSi persiste el error, revisa los mensajes de TypeScript y corrige los tipos.
interface UserProfile {
id: string;
email: string;
first_name: string | null;
last_name: string | null;
display_name: string | null;
nationality: string | null;
date_of_birth: string | null; // Formato: "YYYY-MM-DD"
phone: string | null;
avatar_url: string | null; // Emoji o URL
preferred_language: string | null; // Código ISO (es, en, fr)
preferred_currency: string | null; // Código ISO (EUR, USD, GBP)
timezone: string | null; // IANA timezone (Europe/Madrid)
travel_preferences: Record | null;
created_at: string; // ISO 8601 timestamp
updated_at: string; // ISO 8601 timestamp
}interface Destination {
id: number;
name: string; // "España"
place: string; // "Barcelona"
description: string; // "Ciudad de arte y arquitectura"
image: string; // "/images/destinations/espana.jpg"
}interface UserLocationData {
timezone: string; // "Europe/Madrid"
currency: string; // "EUR"
language: string; // "es"
location?: {
city: string; // "Madrid"
region: string; // "Madrid"
country: string; // "ES" (código ISO-2)
country_name: string; // "Spain"
latitude: number; // 40.4165
longitude: number; // -3.7026
postal: string; // "28001"
};
}interface Country {
code: string; // "ES" (código ISO-2)
name: string; // "España"
flag: string; // "🇪🇸" (emoji)
phone: string; // "+34"
}interface AuthResponse {
access_token: string;
refresh_token: string;
user: UserProfile;
}Marco Aurelio está trabajando en resolver estos issues:
- ⏸️ Google OAuth: Error de duplicate key al registrar usuarios con Google
- ⏸️ Google OAuth: No devuelve refresh_token en algunos casos
- ⏸️ Verify Email: Login permite acceso sin verificar email (debería rechazar)
- ⏸️ Current Location: Endpoint
/api/v1/user/current-locationno implementado (404)
Las siguientes funcionalidades están completamente preparadas en el frontend pero esperan implementación del backend:
Actual: /api/v1/user/*
Futuro: /api/v1/account/*
/api/v1/account/
├── personal (perfil + datos médicos)
├── connections (Google OAuth, redes sociales)
├── messaging (notificaciones, preferencias)
├── security (contraseña, 2FA)
├── history (reservas, búsquedas)
├── payments (métodos de pago, Stripe)
└── documents (pasaportes, visas)
Tiempo estimado frontend: 3-4 horas (cuando backend esté listo)
Roles propuestos:
client→ Usuario normal (hace reservas)admin→ Administrador (hace todo)
Frontend pendiente:
- Dashboard de admin (
/app/admin/*) - Protección de rutas por rol
- Componentes condicionales según rol
Tiempo estimado frontend: 6-8 horas
Endpoints necesarios:
GET /api/v1/hotels/search?city=Paris&checkin=2025-06-01&checkout=2025-06-05
GET /api/v1/hotels/:id
POST /api/v1/hotels/:id/reserve
Frontend pendiente:
- Formulario de búsqueda avanzada
- Grid de resultados con filtros
- Página de detalle de hotel
- Integración con carrito
Tiempo estimado frontend: 8-10 horas
Endpoints necesarios:
GET /api/v1/flights/search?from=MAD&to=CDG&date=2025-06-01
GET /api/v1/flights/:id
POST /api/v1/flights/:id/reserve
Frontend pendiente:
- Formulario de búsqueda (origen, destino, fechas, pasajeros)
- Lista de resultados
- Comparador de precios
- Integración con carrito
Tiempo estimado frontend: 8-10 horas
Endpoints necesarios:
GET /api/v1/offers
GET /api/v1/offers/:id
Frontend pendiente:
- Grid de ofertas destacadas
- Filtros por tipo (hoteles, vuelos, paquetes)
- Countdown timer para ofertas limitadas
Tiempo estimado frontend: 3-4 horas
Endpoints necesarios:
GET /api/v1/favorites
POST /api/v1/favorites (añadir)
DELETE /api/v1/favorites/:id (eliminar)
Frontend pendiente:
- Página de favoritos funcional
- Toggle favorito en hoteles/vuelos
- Persistencia en backend
Tiempo estimado frontend: 4-5 horas
Endpoints necesarios:
GET /api/v1/cart
POST /api/v1/cart (añadir item)
DELETE /api/v1/cart/:id (eliminar item)
POST /api/v1/cart/checkout (procesar pago)
Frontend pendiente:
- Carrito funcional con cálculo de total
- Botón "Pagar" → Stripe
- Resumen de compra
Tiempo estimado frontend: 6-8 horas
Endpoints necesarios:
GET /api/v1/reservations
GET /api/v1/reservations/:id
Frontend pendiente:
- Lista de reservas pasadas
- Detalle de reserva
- Descargar PDF de confirmación
Tiempo estimado frontend: 4-5 horas
Confirmación necesaria: ¿Se usará Stripe?
Endpoints necesarios:
POST /api/v1/payments/create-intent
POST /api/v1/payments/confirm
GET /api/v1/payments/methods
Frontend pendiente:
- Instalar
@stripe/stripe-jsy@stripe/react-stripe-js - Componente de pago (CheckoutForm)
- Gestión de métodos de pago guardados
Tiempo estimado frontend: 6-8 horas
Confirmación necesaria: ¿Es obligatorio para el TFG?
Opciones:
- OpenAI API (ChatGPT)
- Anthropic API (Claude)
- Widget de terceros (Drift, Intercom)
Tiempo estimado frontend: 4-6 horas (si se implementa)
Tiempo total estimado frontend pendiente: 50-70 horas
⚠️ Importante: Todo esto depende 100% de que Marco Aurelio implemente los endpoints correspondientes en el backend.
✅ Hacer:
- Definir interfaces para props de componentes
- Usar tipos explícitos para objetos complejos
- Aprovechar type inference cuando es obvio
- Usar
unknownen lugar deanycuando sea necesario
❌ Evitar:
- Usar
anysin justificación - Dejar
implicit anyen funciones - Ignorar errores de tipo con
@ts-ignore
Ejemplo:
// ✅ BIEN
interface ButtonProps {
text: string;
onClick: () => void;
disabled?: boolean;
}
const Button = ({ text, onClick, disabled = false }: ButtonProps) => {
// ...
}
// ❌ MAL
const Button = ({ text, onClick, disabled }: any) => {
// ...
}✅ Hacer:
- Usar
'use client'solo cuando sea necesario (interactividad) - Mantener componentes pequeños (Single Responsibility)
- Extraer lógica compleja a custom hooks
- Memorizar valores costosos con
useMemo - Evitar re-renders con
React.memocuando sea apropiado
❌ Evitar:
- Poner toda la lógica en un solo componente gigante
- Calcular valores costosos en cada render
- Abusar de
useEffectpara lógica que puede ser síncrona
Ejemplo:
// ✅ BIEN
const ExpensiveComponent = React.memo(({ data }) => {
const processedData = useMemo(() => {
return data.map(item => /* cálculo costoso */);
}, [data]);
return {/* render */};
});
// ❌ MAL
const ExpensiveComponent = ({ data }) => {
const processedData = data.map(item => /* cálculo costoso */);
// Se recalcula en CADA render
return {/* render */};
};✅ Hacer:
- Usar clases utilitarias en lugar de CSS personalizado
- Seguir el patrón mobile-first (
sm:,md:,lg:) - Extraer componentes si repites las mismas clases frecuentemente
- Usar
groupypeerpara estados hover complejos
❌ Evitar:
- Escribir CSS custom cuando Tailwind lo ofrece
- Usar
!important(ajusta la especificidad correctamente) - Ignorar el sistema de diseño (usa los breakpoints estándar)
Ejemplo:
// ✅ BIEN (mobile-first)
{/* text-sm por defecto, text-base en tablet, text-lg en desktop */}
// ❌ MAL (desktop-first)
{/* confuso y no sigue el patrón de Tailwind */}✅ Hacer:
- Commits atómicos (un cambio lógico por commit)
- Mensajes descriptivos (
feat: añadir búsqueda de hoteles) - Usar ramas feature (
feature/search-hotels) - Pull requests para code review
- Rebase antes de merge para historial limpio
❌ Evitar:
- Commits gigantes con múltiples cambios no relacionados
- Mensajes vagos (
fix,update,wip) - Commitear archivos sensibles (
.env.local) - Merge directo a
mainsin review
Ejemplo de nombres de commits:
feat: añadir formulario de búsqueda de hoteles
fix: corregir validación de email en register
refactor: extraer lógica de auth a custom hook
docs: actualizar README con nuevos endpoints
style: formatear componentes con Prettier- Frontend: [Tu nombre]
- Backend: Marco Aurelio
- TFG: Desarrollo de Aplicaciones Web (DAW) 2025
- Next.js: https://nextjs.org/docs
- React: https://react.dev
- TypeScript: https://www.typescriptlang.org/docs
- Tailwind CSS: https://tailwindcss.com/docs
- Framer Motion: https://www.framer.com/motion
- Lucide Icons: https://lucide.dev
- ipapi.co API: https://ipapi.co/api
- Next.js Examples: https://github.com/vercel/next.js/tree/canary/examples
- Tailwind UI Components: https://tailwindui.com (de pago, pero con ejemplos gratuitos)
- React Patterns: https://reactpatterns.com
Proyecto: ProacTrip - Sistema de Gestión de Viajes
TFG: Desarrollo de Aplicaciones Web 2025
Equipo Frontend: Marcos Casas
Equipo Backend: Marco Aurelio
Última actualización: Marzo 2026
Versión: 1.0.0
Licencia: Privado - Proyecto Académico