Interfaz de operación del resort experiencial · React 19 + Vite + Tailwind v4
- Visión general
- Stack tecnológico
- Design System
- Features
- Quick Start
- Variables de entorno
- Estructura del proyecto
- Routing y roles
- Arquitectura de componentes
- Gestión de estado y data fetching
- Responsive design
- Despliegue
- Versionado
- Convenciones de Git
- Autor
Drive Arena Frontend es la SPA que da forma a la operación diaria del resort: gestiona el turno de taquilla, el panel de control del administrador, y el flujo Kanban del técnico de mantenimiento. Consume la API REST del backend mediante Axios autenticado con JWT, y materializa un sistema de diseño propio inspirado en la estética de circuitos nocturnos: dark + rojo de marca + tipografía condensada.
La aplicación está construida con React 19, Vite 7, Tailwind CSS v4 y React Router 7, organizada por rol con tres layouts independientes (DashboardLayout para escritorio, TecnicoLayout para tablet, PublicLayout para landing y reserva pública).
| Categoría | Tecnología | Versión |
|---|---|---|
| Framework | React | 19 |
| Build tool | Vite | 7 |
| Estilos | Tailwind CSS | v4 (CSS variables, sin config JS) |
| Routing | React Router | 7 |
| Formularios | react-hook-form + zod + @hookform/resolvers | 7 / 3 |
| HTTP | Axios | 1.x |
| Animaciones | framer-motion | 12 |
| Iconografía | lucide-react | 0.x |
| Notificaciones | react-hot-toast | 2.x |
| Fechas | react-day-picker + date-fns | 9 |
| Linting | ESLint | 9 |
Cero dependencias UI heavyweight: ni Material UI, ni Chakra, ni Headless UI. Todo el sistema visual está construido sobre Tailwind v4 + tokens CSS personalizados.
| Token Tailwind | Color | Uso |
|---|---|---|
bg-primary |
#FF2D2D (Drive Arena Red) |
CTAs, KPI signature, badges activos, marca |
bg-bg |
#0A0A0F |
Fondo global |
bg-surface-1 |
#13131A |
Cards, modales, headers |
bg-surface-2 |
#1A1A22 |
Hover states, sub-superficies |
border-border-strong |
#2A2A35 |
Bordes y divisores |
text-text |
#F5F5F7 |
Texto principal |
text-text-muted |
#A0A0AB |
Texto secundario |
text-text-dim |
#6B6B75 |
Texto terciario |
| Familia | Token | Uso |
|---|---|---|
| Bebas Neue | font-display |
Headings, KPIs, números grandes |
| Roboto Mono | font-mono |
Etiquetas, metadata, micro-copy técnico |
| Inter | font-sans |
Cuerpo de texto, formularios |
- KPI glow: valores numéricos principales con
text-primary+drop-shadow-[0_0_8px_rgba(255,45,45,0.3)]. - CTA glow: botones
size="lg"conhover:shadow-xl hover:shadow-primary/50. - Micro-interactions: cards con
hover:bg-surface-2/50, botones conactive:scale-[0.98]. - Stagger animations: listas con
framer-motionentrando escalonadas (50 ms). - Skeleton loaders: componente
<SkeletonCard />reutilizable conanimate-pulse.
- ADMIN: panel de control con KPIs en tiempo real, dashboard con widgets editables (ingresos mensuales, top lodges, ventas por edad, mantenimientos pendientes), gestión completa de clientes, ventas, lodges, circuitos, mantenimientos y usuarios sistema.
- TAQUILLA: flujo de venta optimizado (
/taquilla/nueva-compra), gestión de clientes, listado de "mis ventas" y "todas las ventas". - TÉCNICO: vista Kanban a 3 columnas (Pendiente / En curso / Resuelto), asignación dinámica de mantenimientos, reportes de finalización.
- Sidebar drawer con hamburger menu en mobile (
<md), comportamiento idéntico al sidebar fijo en desktop (md:). - Tablas con reflow tabla→cards en mobile (las páginas de listado mantienen
<table>en desktop pero muestran cards apiladas en mobile, sin scroll horizontal). - Modales bottom-sheet en mobile (entran desde abajo) y centrados en desktop, todo con
framer-motion. - Filtros con wrap automático (
flex-wrap) para evitar overflow horizontal. - Formularios con validación zod y feedback de errores inline.
- Notificaciones toast para todas las acciones CRUD.
- Skeleton states durante carga para evitar layout shift.
- Imágenes desde Cloudinary con componente
<ImageWithFallback />.
- Code splitting automático por ruta vía React Router.
- HMR instantáneo con Vite.
- ESLint con reglas de hooks y react-refresh.
- Build producción ~460 ms, gzip ~540 KB total.
- Node.js ≥ 20
- npm ≥ 10
- Backend corriendo en
http://localhost:8080(ver backend repo)
# 1. Clonar
git clone https://github.com/BryanStrk/drive-arena-frontend.git
cd drive-arena-frontend
# 2. Instalar dependencias
npm install
# 3. Configurar variables (ver siguiente sección)
cp .env.example .env
# 4. Arrancar dev server
npm run devLa app quedará disponible en http://localhost:5173.
npm run dev # Dev server con HMR (Vite)
npm run build # Build producción → dist/
npm run preview # Preview del build de producción
npm run lint # ESLint sobre src/# URL del backend
VITE_API_BASE_URL=http://localhost:8080/api
# Cloudinary (solo si hay upload directo desde el cliente)
VITE_CLOUDINARY_CLOUD_NAME=tu_cloud
VITE_CLOUDINARY_UPLOAD_PRESET=tu_preset
# Entorno
VITE_APP_ENV=developmentImportante: Vite expone al cliente solo las variables prefijadas con
VITE_. Nunca pongas secretos como API keys de servidor en estas variables.
vite.config.js incluye un proxy para evitar problemas de CORS en local:
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}drive-arena-frontend/
├── public/
│ └── favicon.svg
├── src/
│ ├── components/
│ │ ├── Button.jsx, Input.jsx, Card.jsx, KpiCard.jsx,
│ │ ├── Badge.jsx, SkeletonCard.jsx, ImageWithFallback.jsx
│ │ ├── circuitos/ ← Modales y forms específicos de circuitos
│ │ ├── clientes/
│ │ ├── compras/
│ │ ├── dashboard/ ← Widgets del panel
│ │ │ ├── MonthlyRevenueWidget.jsx
│ │ │ ├── TopLodgesWidget.jsx
│ │ │ ├── AgeRangeSalesWidget.jsx
│ │ │ └── MantenimientosWidget.jsx
│ │ ├── layout/ ← Sidebar y elementos de chrome
│ │ ├── lodges/
│ │ ├── mantenimiento/ ← MantenimientoCard, modales
│ │ ├── taquilla/ ← Modales detalle venta, ticket, cliente
│ │ ├── tecnico/ ← Kanban, reporte de mantenimiento
│ │ └── usuarios/ ← Modales de gestión usuario sistema
│ ├── layouts/
│ │ ├── DashboardLayout.jsx ← ADMIN + TAQUILLA (sidebar + breadcrumbs)
│ │ ├── TecnicoLayout.jsx ← TÉCNICO (header simple, optimizado tablet)
│ │ └── PublicLayout.jsx ← Landing y reserva pública
│ ├── lib/
│ │ ├── api.js ← Axios instance con interceptors JWT
│ │ ├── motion.js ← Variantes framer-motion reutilizables
│ │ └── utils.js ← Formatters (fechas, moneda, etc)
│ ├── pages/
│ │ ├── Login.jsx, Landing.jsx, NotFound.jsx
│ │ ├── Dashboard.jsx
│ │ ├── ClientesPage.jsx, UsuariosPage.jsx,
│ │ ├── ComprasPage.jsx, LodgesPage.jsx, CircuitosPage.jsx,
│ │ ├── MantenimientoPage.jsx, RankingPage.jsx
│ │ ├── taquilla/
│ │ │ ├── NuevaCompraPage.jsx
│ │ │ ├── MisComprasPage.jsx
│ │ │ ├── TodasLasVentasPage.jsx
│ │ │ └── TaquillaClientesPage.jsx
│ │ └── tecnico/
│ │ └── KanbanPage.jsx
│ ├── hooks/
│ │ ├── useAuth.js ← Context auth + login/logout
│ │ ├── useApi.js ← Wrapper sobre Axios con loading/error
│ │ └── useDebounce.js
│ ├── router/
│ │ └── index.jsx ← Definición de rutas + ProtectedRoute por rol
│ ├── styles/
│ │ └── globals.css ← Tokens CSS + @import tailwindcss
│ ├── App.jsx
│ └── main.jsx
├── vercel.json ← Rewrite SPA + headers
├── vite.config.js ← Proxy /api + alias @/
├── eslint.config.js
├── package.json
└── README.md
El routing usa React Router 7 con un wrapper <ProtectedRoute> que valida el rol antes de renderizar.
/ → PublicLayout (Landing)
/login → Login (sin layout)
/reservar → PublicLayout (Reserva pública)
/dashboard → DashboardLayout · ADMIN
├── /clientes
├── /lodges
├── /circuitos
├── /usuarios
├── /mantenimiento
└── /compras
/taquilla → DashboardLayout · TAQUILLA
├── /nueva-compra
├── /mis-compras
├── /todas-las-ventas
└── /clientes
/tecnico → TecnicoLayout · TÉCNICO
└── /kanban
<ProtectedRoute allowedRoles={['ADMIN']}>
<DashboardLayout>
<UsuariosPage />
</DashboardLayout>
</ProtectedRoute>Si el usuario no tiene rol, redirección a /login. Si tiene un rol que no está en allowedRoles, redirección a su home por rol.
<Button variant size>— variants:primary(rojo),secondary(outline),ghost,danger. Sizes:sm,md,lg.<Input>— wrapper sobre<input>con focus ring de marca, error state, label embebido.<Card>— superficie conbg-surface-1,border-border-strong,rounded-card.<Badge variant dot>— pills de estado con punto de color opcional.<KpiCard>— número grande condrop-shadowrojo signature.<SkeletonCard count>— loader conanimate-pulse.
Todos los modales del proyecto siguen el mismo patrón con framer-motion:
<AnimatePresence>
{isOpen && (
<motion.div
className="fixed inset-0 z-50 flex items-end sm:items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
className="bg-surface-1 rounded-t-card sm:rounded-card max-h-[90vh] overflow-y-auto"
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
>
{/* contenido */}
</motion.div>
</motion.div>
)}
</AnimatePresence>→ Bottom-sheet en mobile, centrado en desktop.
<PageHeader title="Clientes" cta={<Button>Nuevo Cliente</Button>} />
<SearchAndFilters />
{isLoading
? <SkeletonList count={5} />
: <>
<table className="hidden md:block">...</table> {/* desktop */}
<motion.ul className="md:hidden">...</motion.ul> {/* mobile cards con stagger */}
</>
}
<Pagination />- Auth context (
useAuth) — usuario actual, token JWT, login, logout, refresh. - Sin Redux ni Zustand — el estado global se reduce a auth; el resto se gestiona localmente con
useState/useReducer.
- Axios instance (
lib/api.js) con interceptor que inyecta el JWT en cada request y maneja401(logout automático + redirect a/login). - Patrón ad-hoc con
useEffectyuseState({ data, loading, error }). - No se usa SWR ni React Query: el alcance del proyecto no lo justifica y mantiene la curva de aprendizaje baja para el tribunal.
const [clientes, setClientes] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
api.get('/clientes', { params: { page, search } })
.then(res => setClientes(res.data.content))
.catch(err => toast.error('Error al cargar clientes'))
.finally(() => setIsLoading(false));
}, [page, search]);| Token | Pixels | Uso |
|---|---|---|
sm: |
≥ 640px | Ajustes de spacing y tipografía |
md: |
≥ 768px | Breakpoint principal mobile/desktop |
lg: |
≥ 1024px | Grids multi-columna |
xl: |
≥ 1280px | Dashboard a 3 columnas |
A partir de la versión v2.1.0, todo el código sigue una estrategia mobile-first: clases base aplican al mobile, los modificadores sm: / md: aplican a desktop. Esto evita el anti-patrón de "desktop primero con max-md: para mobile".
- Sidebar: drawer overlay con backdrop en mobile (
<md), sidebar fijo en desktop (md:). - Tablas:
<table className="hidden md:block">+<ul className="md:hidden">para reflow a cards. - Modales:
items-end sm:items-center(bottom-sheet vs centrado). - Filtros:
flex-wrappara wrap automático sin overflow. - Headers:
flex-col sm:flex-rowpara stack vertical en mobile.
El frontend se despliega automáticamente en Vercel con auto-deploy desde la rama main.
// vercel.json
{
"rewrites": [{ "source": "/(.*)", "destination": "/" }]
}El rewrite es necesario para que React Router gestione las rutas SPA sin que Vercel devuelva 404 en refresh.
npm run build # → dist/El output (dist/) es estático y puede servirse desde cualquier CDN o servidor estático.
| Tag | Hito |
|---|---|
v1.0.0 |
Primera versión funcional con CRUDs básicos |
v1.5.0 |
Sistema de auth + rutas protegidas por rol |
v1.8.0 |
Dashboard con widgets y métricas |
v2.0.0 |
Multi-técnico, roles TÉCNICO + TAQUILLA, paginación |
v2.1.0 |
Responsive mobile completo + visual polish (micro-interactions, stagger animations, signature effects) |
Mismas convenciones que el backend: Conventional Commits + GitFlow simplificado + merges --no-ff.
| Tipo | Uso |
|---|---|
feat |
Nueva feature de UI |
fix |
Bug visual o funcional |
refactor |
Reorganización de componentes |
chore |
Dependencias, config |
style |
Cambios CSS sin lógica |
docs |
README, comentarios |
git checkout -b feature/nombre-acotado
# trabajo + commits temáticos por área
git checkout main
git merge feature/nombre-acotado --no-ff
git tag -a vX.Y.Z -m "..."
git push origin main --tagsBryan Alejandro Paico Albines — Desarrollo de Aplicaciones Web (DAW) · 2026
- GitHub: @BryanStrk
- Proyecto backend: drive-arena-backend
Proyecto académico desarrollado como Trabajo academico de Desarrollo de Aplicaciones Web. Uso no comercial.
Drive Arena · Conduce · Compite · Domina