Add responsive design
This commit is contained in:
324
docs/RESPONSIVE.md
Normal file
324
docs/RESPONSIVE.md
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
# Responsive Design Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Karibeo está completamente optimizado para funcionar en dispositivos móviles, tablets y escritorio. Esta documentación detalla las estrategias de diseño responsivo implementadas.
|
||||||
|
|
||||||
|
## Breakpoints
|
||||||
|
|
||||||
|
El proyecto utiliza breakpoints estándar de Tailwind CSS:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const BREAKPOINTS = {
|
||||||
|
mobile: 768, // < 768px
|
||||||
|
tablet: 1024, // 768px - 1023px
|
||||||
|
desktop: 1280, // 1024px - 1279px
|
||||||
|
largeDesktop: 1280+ // >= 1280px
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hooks Personalizados
|
||||||
|
|
||||||
|
### useResponsive
|
||||||
|
|
||||||
|
Hook para detectar el breakpoint actual:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useResponsive } from '@/hooks/useResponsive';
|
||||||
|
|
||||||
|
const { isMobile, isTablet, isDesktop, width } = useResponsive();
|
||||||
|
```
|
||||||
|
|
||||||
|
### useOrientation
|
||||||
|
|
||||||
|
Hook para detectar orientación del dispositivo:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useOrientation } from '@/hooks/useResponsive';
|
||||||
|
|
||||||
|
const orientation = useOrientation(); // 'portrait' | 'landscape'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend Responsive
|
||||||
|
|
||||||
|
### Dashboard Layout
|
||||||
|
|
||||||
|
- **Sidebar**: Oculto en móvil, colapsable en desktop
|
||||||
|
- **Top Navigation**: Componentes adaptados según tamaño de pantalla
|
||||||
|
- **Content Area**: Padding responsive (3 en móvil, 6 en desktop)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Ejemplo de uso
|
||||||
|
<div className="p-3 md:p-6">
|
||||||
|
{/* Content adapts padding based on screen size */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grid Layouts
|
||||||
|
|
||||||
|
Todos los grids utilizan configuración responsive:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ExploreSection
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
|
||||||
|
{/* Cards automatically adjust */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// PlacesSection
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-0">
|
||||||
|
{/* Stacks vertically on mobile, side-by-side on desktop */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
Tamaños de texto adaptados:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<h1 className="text-lg md:text-2xl font-bold">
|
||||||
|
{/* Smaller on mobile, larger on desktop */}
|
||||||
|
</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Images
|
||||||
|
|
||||||
|
Alturas responsive en tarjetas:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="h-[350px] sm:h-[400px] lg:h-[458px]">
|
||||||
|
{/* Image height scales with screen size */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Responsive
|
||||||
|
|
||||||
|
### Middleware API
|
||||||
|
|
||||||
|
El archivo `src/middleware/responsiveAPI.ts` proporciona funciones para optimizar las respuestas del backend:
|
||||||
|
|
||||||
|
#### detectDevice
|
||||||
|
|
||||||
|
Detecta tipo de dispositivo desde User-Agent:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { detectDevice } from '@/middleware/responsiveAPI';
|
||||||
|
|
||||||
|
const deviceType = detectDevice(userAgent, width);
|
||||||
|
// Returns: 'mobile' | 'tablet' | 'desktop'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### optimizePayload
|
||||||
|
|
||||||
|
Optimiza payload para dispositivos móviles:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { optimizePayload } from '@/middleware/responsiveAPI';
|
||||||
|
|
||||||
|
const optimized = optimizePayload(data, deviceType);
|
||||||
|
// Reduces image quality, limits array sizes for mobile
|
||||||
|
```
|
||||||
|
|
||||||
|
#### addDeviceHeaders
|
||||||
|
|
||||||
|
Añade headers de dispositivo a requests:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { addDeviceHeaders } from '@/middleware/responsiveAPI';
|
||||||
|
|
||||||
|
const headers = addDeviceHeaders();
|
||||||
|
// Adds: X-Device-Type, X-Screen-Width, X-User-Agent
|
||||||
|
```
|
||||||
|
|
||||||
|
#### getAdaptivePagination
|
||||||
|
|
||||||
|
Paginación adaptativa según dispositivo:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getAdaptivePagination } from '@/middleware/responsiveAPI';
|
||||||
|
|
||||||
|
const { limit, offset } = getAdaptivePagination(deviceType);
|
||||||
|
// mobile: 10, tablet: 20, desktop: 50
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
### responsive.ts
|
||||||
|
|
||||||
|
Funciones útiles para comportamiento responsive:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
responsiveClass,
|
||||||
|
getResponsiveValue,
|
||||||
|
isTouchDevice,
|
||||||
|
formatTextForMobile
|
||||||
|
} from '@/utils/responsive';
|
||||||
|
|
||||||
|
// Clases responsive
|
||||||
|
const classes = responsiveClass('base', 'mobile-class', 'tablet-class', 'desktop-class');
|
||||||
|
|
||||||
|
// Valores responsive
|
||||||
|
const value = getResponsiveValue(10, 20, 50, width);
|
||||||
|
|
||||||
|
// Detectar touch
|
||||||
|
const isTouch = isTouchDevice();
|
||||||
|
|
||||||
|
// Formatear texto
|
||||||
|
const text = formatTextForMobile(longText, 100, isMobile);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mejores Prácticas
|
||||||
|
|
||||||
|
### 1. Mobile-First
|
||||||
|
|
||||||
|
Siempre diseñar primero para móvil:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ Correcto
|
||||||
|
<div className="p-3 md:p-6">
|
||||||
|
|
||||||
|
// ❌ Incorrecto
|
||||||
|
<div className="p-6 md:p-3">
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Ocultar Elementos
|
||||||
|
|
||||||
|
Usar clases de Tailwind para ocultar:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Oculto en móvil
|
||||||
|
<div className="hidden md:block">
|
||||||
|
|
||||||
|
// Solo visible en móvil
|
||||||
|
<div className="block md:hidden">
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Espaciado Responsive
|
||||||
|
|
||||||
|
Adaptar gaps, padding y margin:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="gap-2 md:gap-4 lg:gap-6">
|
||||||
|
{/* Spacing increases with screen size */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Touch Targets
|
||||||
|
|
||||||
|
Asegurar áreas táctiles suficientes en móvil (mínimo 44x44px):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<button className="w-10 h-10 md:w-12 md:h-12">
|
||||||
|
{/* Adequate touch area on mobile */}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Overflow Handling
|
||||||
|
|
||||||
|
Prevenir overflow horizontal:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<div className="min-w-max">
|
||||||
|
{/* Wide content scrolls horizontally */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Breakpoints de Prueba
|
||||||
|
|
||||||
|
Probar en estos anchos mínimos:
|
||||||
|
|
||||||
|
- **Mobile**: 320px, 375px, 414px
|
||||||
|
- **Tablet**: 768px, 834px
|
||||||
|
- **Desktop**: 1024px, 1280px, 1920px
|
||||||
|
|
||||||
|
### Orientación
|
||||||
|
|
||||||
|
Probar tanto portrait como landscape en móvil/tablet.
|
||||||
|
|
||||||
|
### Touch Events
|
||||||
|
|
||||||
|
Verificar que todos los elementos interactivos funcionen con touch.
|
||||||
|
|
||||||
|
## Rendimiento
|
||||||
|
|
||||||
|
### Lazy Loading
|
||||||
|
|
||||||
|
Implementar lazy loading para imágenes:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<img loading="lazy" src={image} alt={alt} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
React.lazy para componentes pesados:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const HeavyComponent = lazy(() => import('./HeavyComponent'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responsive Images
|
||||||
|
|
||||||
|
Usar srcset para diferentes resoluciones:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<img
|
||||||
|
src="image-mobile.jpg"
|
||||||
|
srcSet="image-mobile.jpg 375w, image-tablet.jpg 768w, image-desktop.jpg 1920w"
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accesibilidad
|
||||||
|
|
||||||
|
### Viewport Meta Tag
|
||||||
|
|
||||||
|
Ya incluido en index.html:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Focus States
|
||||||
|
|
||||||
|
Asegurar estados de focus visibles:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<button className="focus:ring-2 focus:ring-primary focus:outline-none">
|
||||||
|
```
|
||||||
|
|
||||||
|
### ARIA Labels
|
||||||
|
|
||||||
|
Usar labels descriptivos para lectores de pantalla:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<button aria-label="Open menu">
|
||||||
|
<Menu />
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mantenimiento
|
||||||
|
|
||||||
|
### Auditorías Regulares
|
||||||
|
|
||||||
|
- Ejecutar Lighthouse para móvil y desktop
|
||||||
|
- Verificar responsive en DevTools
|
||||||
|
- Probar en dispositivos reales
|
||||||
|
|
||||||
|
### Actualización de Breakpoints
|
||||||
|
|
||||||
|
Si se necesitan breakpoints custom, actualizar en:
|
||||||
|
|
||||||
|
1. `tailwind.config.ts`
|
||||||
|
2. `src/hooks/useResponsive.tsx`
|
||||||
|
3. Documentación
|
||||||
|
|
||||||
|
## Recursos
|
||||||
|
|
||||||
|
- [Tailwind Responsive Design](https://tailwindcss.com/docs/responsive-design)
|
||||||
|
- [MDN Media Queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries)
|
||||||
|
- [Web.dev Responsive Images](https://web.dev/responsive-images/)
|
||||||
@@ -268,8 +268,8 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
<div className="decoration blur-2"></div>
|
<div className="decoration blur-2"></div>
|
||||||
<div className="decoration blur-3"></div>
|
<div className="decoration blur-3"></div>
|
||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar - Hidden on mobile, collapsible on desktop */}
|
||||||
<nav className={`fixed top-0 left-0 z-50 h-screen bg-white border-r-4 border-white transition-all duration-300 ${sidebarCollapsed ? 'w-16' : 'w-80'}`} style={{ minWidth: sidebarCollapsed ? '64px' : '320px', maxWidth: sidebarCollapsed ? '64px' : '320px', backdropFilter: 'blur(15px)', backgroundColor: 'rgba(255, 255, 255, 0.9)' }}>
|
<nav className={`fixed top-0 left-0 z-50 h-screen bg-white border-r-4 border-white transition-all duration-300 ${sidebarCollapsed ? 'w-16' : 'w-80'} md:block hidden`} style={{ minWidth: sidebarCollapsed ? '64px' : '320px', maxWidth: sidebarCollapsed ? '64px' : '320px', backdropFilter: 'blur(15px)', backgroundColor: 'rgba(255, 255, 255, 0.9)' }}>
|
||||||
{/* Sidebar Header */}
|
{/* Sidebar Header */}
|
||||||
<div className="flex items-center justify-between p-6 h-20 border-b border-gray-100">
|
<div className="flex items-center justify-between p-6 h-20 border-b border-gray-100">
|
||||||
<Link to="/dashboard" className={`flex items-center space-x-2 ${sidebarCollapsed ? 'justify-center' : ''}`}>
|
<Link to="/dashboard" className={`flex items-center space-x-2 ${sidebarCollapsed ? 'justify-center' : ''}`}>
|
||||||
@@ -508,87 +508,91 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area - Responsive */}
|
||||||
<div className={`transition-all duration-300 ${sidebarCollapsed ? 'ml-16' : 'ml-80'}`}>
|
<div className={`transition-all duration-300 ${sidebarCollapsed ? 'md:ml-16 ml-0' : 'md:ml-80 ml-0'}`}>
|
||||||
{/* Top Navigation */}
|
{/* Top Navigation - Responsive */}
|
||||||
<nav className="h-20 px-6 py-4 z-10 relative" style={{ backgroundColor: 'rgba(255, 255, 255, 0.7)', backdropFilter: 'blur(15px)' }}>
|
<nav className="h-16 md:h-20 px-4 md:px-6 py-3 md:py-4 z-10 relative" style={{ backgroundColor: 'rgba(255, 255, 255, 0.7)', backdropFilter: 'blur(15px)' }}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Left Side */}
|
{/* Left Side - Responsive */}
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-2 md:space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||||
className="p-2 rounded-full text-white transition-colors"
|
className="p-1.5 md:p-2 rounded-full text-white transition-colors"
|
||||||
style={{ backgroundColor: '#F84525' }}
|
style={{ backgroundColor: '#F84525' }}
|
||||||
>
|
>
|
||||||
<Menu className="w-5 h-5" />
|
<Menu className="w-4 h-4 md:w-5 md:h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search - Hidden on mobile */}
|
||||||
<div className="relative">
|
<div className="relative hidden md:block">
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<Search className="h-4 w-4" style={{ color: '#69534f' }} />
|
<Search className="h-4 w-4" style={{ color: '#69534f' }} />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={`${t('search')} (Ctrl+/)`}
|
placeholder={`${t('search')} (Ctrl+/)`}
|
||||||
className="block w-80 pl-12 pr-16 py-3 border border-white rounded-xl text-sm placeholder-gray-500 focus:outline-none focus:ring-1 focus:border-red-500"
|
className="block w-48 lg:w-80 pl-12 pr-16 py-3 border border-white rounded-xl text-sm placeholder-gray-500 focus:outline-none focus:ring-1 focus:border-red-500"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderColor: '#fff',
|
borderColor: '#fff',
|
||||||
height: '48px',
|
height: '44px',
|
||||||
borderRadius: '0.8rem'
|
borderRadius: '0.8rem'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="absolute inset-y-0 right-0 pr-3 flex items-center text-xs font-bold px-2 py-1 rounded" style={{ backgroundColor: '#f8f4f3', borderColor: '#f8f4f3', top: '50%', transform: 'translateY(-50%)', right: '8px', fontSize: '12px' }}>
|
<span className="absolute inset-y-0 right-0 pr-3 flex items-center text-xs font-bold px-2 py-1 rounded hidden lg:flex" style={{ backgroundColor: '#f8f4f3', borderColor: '#f8f4f3', top: '50%', transform: 'translateY(-50%)', right: '8px', fontSize: '12px' }}>
|
||||||
(Ctrl+/)
|
(Ctrl+/)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Side */}
|
{/* Right Side - Responsive */}
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-2 md:space-x-4">
|
||||||
{/* Language Selector */}
|
{/* Language Selector - Hidden on small mobile */}
|
||||||
<LanguageSelector />
|
<div className="hidden sm:block">
|
||||||
|
<LanguageSelector />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Currency Selector */}
|
{/* Currency Selector - Hidden on small mobile */}
|
||||||
<CurrencySelector />
|
<div className="hidden sm:block">
|
||||||
|
<CurrencySelector />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Refresh (Admin) */}
|
{/* Refresh (Admin) - Hidden on mobile */}
|
||||||
<button
|
<button
|
||||||
onClick={() => window.dispatchEvent(new CustomEvent('admin:refresh'))}
|
onClick={() => window.dispatchEvent(new CustomEvent('admin:refresh'))}
|
||||||
className="p-2 rounded-xl transition-colors"
|
className="hidden md:block p-2 rounded-xl transition-colors"
|
||||||
style={{ backgroundColor: '#21272f', borderColor: '#21272f', color: '#F84525' }}
|
style={{ backgroundColor: '#21272f', borderColor: '#21272f', color: '#F84525' }}
|
||||||
title="Actualizar datos"
|
title="Actualizar datos"
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-5 h-5" />
|
<RefreshCw className="w-4 h-4 md:w-5 md:h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Notifications */}
|
{/* Notifications */}
|
||||||
<button className="p-2 rounded-xl transition-colors relative" style={{ backgroundColor: '#21272f', borderColor: '#21272f', color: '#F84525' }} title="Notificaciones">
|
<button className="p-1.5 md:p-2 rounded-xl transition-colors relative" style={{ backgroundColor: '#21272f', borderColor: '#21272f', color: '#F84525' }} title="Notificaciones">
|
||||||
<Bell className="w-5 h-5" />
|
<Bell className="w-4 h-4 md:w-5 md:h-5" />
|
||||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-[10px] px-1 py-0.5 rounded-full">!</span>
|
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-[10px] px-1 py-0.5 rounded-full">!</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Theme Toggle */}
|
{/* Theme Toggle - Hidden on mobile */}
|
||||||
<button className="p-2 rounded-xl transition-colors" style={{ backgroundColor: '#21272f', borderColor: '#21272f', color: '#F84525' }}>
|
<button className="hidden md:block p-2 rounded-xl transition-colors" style={{ backgroundColor: '#21272f', borderColor: '#21272f', color: '#F84525' }}>
|
||||||
<Sun className="w-5 h-5" />
|
<Sun className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Fullscreen */}
|
{/* Fullscreen - Hidden on mobile */}
|
||||||
<button className="p-2 rounded-xl transition-colors" style={{ backgroundColor: '#21272f', borderColor: '#21272f', color: '#F84525' }}>
|
<button className="hidden lg:block p-2 rounded-xl transition-colors" style={{ backgroundColor: '#21272f', borderColor: '#21272f', color: '#F84525' }}>
|
||||||
<Maximize className="w-5 h-5" />
|
<Maximize className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* User Profile */}
|
{/* User Profile - Responsive */}
|
||||||
<div className="flex items-center space-x-3 pl-4">
|
<div className="flex items-center space-x-2 md:space-x-3 pl-2 md:pl-4">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-2 md:space-x-3">
|
||||||
<div className="w-10 h-10 bg-gradient-to-br from-orange-400 to-red-500 rounded-full flex items-center justify-center relative">
|
<div className="w-8 h-8 md:w-10 md:h-10 bg-gradient-to-br from-orange-400 to-red-500 rounded-full flex items-center justify-center relative">
|
||||||
<span className="text-white font-semibold text-sm">
|
<span className="text-white font-semibold text-xs md:text-sm">
|
||||||
{user?.name?.[0] || user?.email?.[0] || 'U'}
|
{user?.name?.[0] || user?.email?.[0] || 'U'}
|
||||||
</span>
|
</span>
|
||||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 border-2 border-white rounded-full"></div>
|
<div className="absolute -bottom-1 -right-1 w-3 h-3 md:w-4 md:h-4 bg-green-500 border-2 border-white rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left hidden md:block">
|
||||||
<div className="font-semibold text-gray-800 text-sm flex items-center space-x-2">
|
<div className="font-semibold text-gray-800 text-sm flex items-center space-x-2">
|
||||||
<span>{user?.name || 'Usuario'}</span>
|
<span>{user?.name || 'Usuario'}</span>
|
||||||
{user?.role === 'super_admin' && (
|
{user?.role === 'super_admin' && (
|
||||||
@@ -605,8 +609,8 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Page Content */}
|
{/* Page Content - Responsive padding */}
|
||||||
<main className="p-6">
|
<main className="p-3 md:p-6">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ const ExploreSection = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Carousel Container */}
|
{/* Carousel Container - Fully responsive grid */}
|
||||||
<div className="owl-carousel-container relative">
|
<div className="owl-carousel-container relative">
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 overflow-x-auto pb-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 overflow-x-auto pb-4">
|
||||||
{/* Region Card 1 */}
|
{/* Region Card 1 - Responsive height */}
|
||||||
<div className="region-card rounded-xl overflow-hidden relative text-white min-w-[300px] h-[458px] group cursor-pointer">
|
<div className="region-card rounded-xl overflow-hidden relative text-white min-w-[280px] sm:min-w-[300px] h-[350px] sm:h-[400px] lg:h-[458px] group cursor-pointer">
|
||||||
<div className="region-card-image h-full">
|
<div className="region-card-image h-full">
|
||||||
<img
|
<img
|
||||||
src="https://images.visitarepublicadominicana.org/Punta-Cana-Republica-Dominicana.jpg"
|
src="https://images.visitarepublicadominicana.org/Punta-Cana-Republica-Dominicana.jpg"
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const PlacesSection = () => {
|
|||||||
return (
|
return (
|
||||||
<section className="py-20 bg-gray-50 relative overflow-hidden">
|
<section className="py-20 bg-gray-50 relative overflow-hidden">
|
||||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||||
<div className="grid lg:grid-cols-12 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 md:gap-8">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<div className="lg:col-span-4 sidebar">
|
<div className="lg:col-span-4 sidebar">
|
||||||
<div className="text-center lg:text-left mb-12">
|
<div className="text-center lg:text-left mb-12">
|
||||||
@@ -97,7 +97,7 @@ const PlacesSection = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{places.map((place, index) => (
|
{places.map((place, index) => (
|
||||||
<a href={`/offer/${index + 1}`} key={index} className="bg-white rounded-xl overflow-hidden border-0 shadow-soft hover:shadow-medium transition-all duration-300 group cursor-pointer block">
|
<a href={`/offer/${index + 1}`} key={index} className="bg-white rounded-xl overflow-hidden border-0 shadow-soft hover:shadow-medium transition-all duration-300 group cursor-pointer block">
|
||||||
<div className="grid md:grid-cols-12 gap-0">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-0">
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
<div className="md:col-span-5 relative bg-white">
|
<div className="md:col-span-5 relative bg-white">
|
||||||
<div className="overflow-hidden relative h-64 md:h-full">
|
<div className="overflow-hidden relative h-64 md:h-full">
|
||||||
@@ -118,8 +118,8 @@ const PlacesSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content - Responsive padding */}
|
||||||
<div className="md:col-span-7 p-6 flex flex-col">
|
<div className="md:col-span-7 p-4 md:p-6 flex flex-col">
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<div className="flex gap-2 justify-end mb-4">
|
<div className="flex gap-2 justify-end mb-4">
|
||||||
<button className="w-10 h-10 bg-white shadow-sm rounded-full flex items-center justify-center text-primary hover:bg-primary hover:text-white transition-colors">
|
<button className="w-10 h-10 bg-white shadow-sm rounded-full flex items-center justify-center text-primary hover:bg-primary hover:text-white transition-colors">
|
||||||
|
|||||||
75
src/hooks/useResponsive.tsx
Normal file
75
src/hooks/useResponsive.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export interface ResponsiveBreakpoints {
|
||||||
|
isMobile: boolean;
|
||||||
|
isTablet: boolean;
|
||||||
|
isDesktop: boolean;
|
||||||
|
isLargeDesktop: boolean;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BREAKPOINTS = {
|
||||||
|
mobile: 768,
|
||||||
|
tablet: 1024,
|
||||||
|
desktop: 1280,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for responsive breakpoint detection
|
||||||
|
* Provides granular control over responsive behavior
|
||||||
|
*/
|
||||||
|
export function useResponsive(): ResponsiveBreakpoints {
|
||||||
|
const [breakpoints, setBreakpoints] = useState<ResponsiveBreakpoints>({
|
||||||
|
isMobile: false,
|
||||||
|
isTablet: false,
|
||||||
|
isDesktop: false,
|
||||||
|
isLargeDesktop: false,
|
||||||
|
width: typeof window !== 'undefined' ? window.innerWidth : 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
|
||||||
|
setBreakpoints({
|
||||||
|
isMobile: width < BREAKPOINTS.mobile,
|
||||||
|
isTablet: width >= BREAKPOINTS.mobile && width < BREAKPOINTS.tablet,
|
||||||
|
isDesktop: width >= BREAKPOINTS.tablet && width < BREAKPOINTS.desktop,
|
||||||
|
isLargeDesktop: width >= BREAKPOINTS.desktop,
|
||||||
|
width,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
handleResize();
|
||||||
|
|
||||||
|
// Listen for resize events
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return breakpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for orientation detection
|
||||||
|
*/
|
||||||
|
export function useOrientation() {
|
||||||
|
const [orientation, setOrientation] = useState<'portrait' | 'landscape'>('portrait');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOrientationChange = () => {
|
||||||
|
setOrientation(
|
||||||
|
window.innerHeight > window.innerWidth ? 'portrait' : 'landscape'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleOrientationChange();
|
||||||
|
window.addEventListener('resize', handleOrientationChange);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', handleOrientationChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return orientation;
|
||||||
|
}
|
||||||
103
src/middleware/responsiveAPI.ts
Normal file
103
src/middleware/responsiveAPI.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Middleware for handling responsive API requests
|
||||||
|
* Ensures backend responds appropriately to different device types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface DeviceInfo {
|
||||||
|
type: 'mobile' | 'tablet' | 'desktop';
|
||||||
|
userAgent: string;
|
||||||
|
screenWidth?: number;
|
||||||
|
screenHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect device type from User-Agent and viewport
|
||||||
|
*/
|
||||||
|
export function detectDevice(userAgent: string, width?: number): DeviceInfo['type'] {
|
||||||
|
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
||||||
|
const tabletRegex = /iPad|Android(?!.*Mobile)/i;
|
||||||
|
|
||||||
|
if (width) {
|
||||||
|
if (width < 768) return 'mobile';
|
||||||
|
if (width < 1024) return 'tablet';
|
||||||
|
return 'desktop';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabletRegex.test(userAgent)) return 'tablet';
|
||||||
|
if (mobileRegex.test(userAgent)) return 'mobile';
|
||||||
|
return 'desktop';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimize response payload based on device type
|
||||||
|
* Reduces data transfer for mobile devices
|
||||||
|
*/
|
||||||
|
export function optimizePayload<T extends Record<string, any>>(
|
||||||
|
data: T,
|
||||||
|
deviceType: DeviceInfo['type']
|
||||||
|
): T {
|
||||||
|
if (deviceType === 'mobile') {
|
||||||
|
// Remove heavy fields for mobile
|
||||||
|
const optimized = { ...data } as any;
|
||||||
|
|
||||||
|
// Remove high-res images, use thumbnails instead
|
||||||
|
if ('images' in data && Array.isArray(data.images)) {
|
||||||
|
optimized.images = (data.images as any[]).map((img: any) => ({
|
||||||
|
...img,
|
||||||
|
url: img.thumbnail || img.url,
|
||||||
|
highRes: undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit array sizes for mobile
|
||||||
|
if ('items' in data && Array.isArray(data.items)) {
|
||||||
|
optimized.items = (data.items as any[]).slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return optimized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add device info to API request headers
|
||||||
|
*/
|
||||||
|
export function addDeviceHeaders(headers: HeadersInit = {}): HeadersInit {
|
||||||
|
const width = typeof window !== 'undefined' ? window.innerWidth : undefined;
|
||||||
|
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
|
||||||
|
const deviceType = detectDevice(userAgent, width);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...headers,
|
||||||
|
'X-Device-Type': deviceType,
|
||||||
|
'X-Screen-Width': width?.toString() || '',
|
||||||
|
'X-User-Agent': userAgent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adaptive pagination based on device
|
||||||
|
*/
|
||||||
|
export function getAdaptivePagination(deviceType: DeviceInfo['type']) {
|
||||||
|
const limits = {
|
||||||
|
mobile: 10,
|
||||||
|
tablet: 20,
|
||||||
|
desktop: 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
limit: limits[deviceType],
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image quality based on device and connection
|
||||||
|
*/
|
||||||
|
export function getImageQuality(deviceType: DeviceInfo['type'], connectionType?: string): number {
|
||||||
|
if (connectionType === 'slow-2g' || connectionType === '2g') return 30;
|
||||||
|
if (deviceType === 'mobile') return 60;
|
||||||
|
if (deviceType === 'tablet') return 80;
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
93
src/utils/responsive.ts
Normal file
93
src/utils/responsive.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Responsive utility functions
|
||||||
|
* Centralized helpers for responsive behavior
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get responsive class names based on condition
|
||||||
|
*/
|
||||||
|
export function responsiveClass(
|
||||||
|
base: string,
|
||||||
|
mobile?: string,
|
||||||
|
tablet?: string,
|
||||||
|
desktop?: string
|
||||||
|
): string {
|
||||||
|
const classes = [base];
|
||||||
|
|
||||||
|
if (mobile) classes.push(`max-md:${mobile}`);
|
||||||
|
if (tablet) classes.push(`md:${tablet} max-lg:${tablet}`);
|
||||||
|
if (desktop) classes.push(`lg:${desktop}`);
|
||||||
|
|
||||||
|
return classes.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get responsive value based on screen width
|
||||||
|
*/
|
||||||
|
export function getResponsiveValue<T>(
|
||||||
|
mobile: T,
|
||||||
|
tablet: T,
|
||||||
|
desktop: T,
|
||||||
|
width: number
|
||||||
|
): T {
|
||||||
|
if (width < 768) return mobile;
|
||||||
|
if (width < 1024) return tablet;
|
||||||
|
return desktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce function for resize events
|
||||||
|
*/
|
||||||
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeout: NodeJS.Timeout;
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func(...args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if device supports touch
|
||||||
|
*/
|
||||||
|
export function isTouchDevice(): boolean {
|
||||||
|
return (
|
||||||
|
'ontouchstart' in window ||
|
||||||
|
navigator.maxTouchPoints > 0 ||
|
||||||
|
(navigator as any).msMaxTouchPoints > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get optimal column count for grid based on width
|
||||||
|
*/
|
||||||
|
export function getGridColumns(width: number, itemMinWidth: number = 300): number {
|
||||||
|
return Math.max(1, Math.floor(width / itemMinWidth));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format text for mobile (truncate if needed)
|
||||||
|
*/
|
||||||
|
export function formatTextForMobile(
|
||||||
|
text: string,
|
||||||
|
maxLength: number = 100,
|
||||||
|
isMobile: boolean
|
||||||
|
): string {
|
||||||
|
if (!isMobile || text.length <= maxLength) return text;
|
||||||
|
return text.substring(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get responsive font size
|
||||||
|
*/
|
||||||
|
export function getResponsiveFontSize(
|
||||||
|
baseSize: number,
|
||||||
|
width: number
|
||||||
|
): number {
|
||||||
|
if (width < 768) return baseSize * 0.875; // 87.5% for mobile
|
||||||
|
if (width < 1024) return baseSize * 0.9375; // 93.75% for tablet
|
||||||
|
return baseSize;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user