Improve the demo feature of the project
This commit is contained in:
105
frontend/src/components/demo/DemoErrorScreen.tsx
Normal file
105
frontend/src/components/demo/DemoErrorScreen.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { Button, Card, CardBody } from '../ui';
|
||||
import { PublicLayout } from '../layout';
|
||||
import { AlertCircle, RefreshCw, Home, HelpCircle } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
error: string;
|
||||
details?: Array<{ service: string; error_message: string }>;
|
||||
onRetry: () => void;
|
||||
isRetrying?: boolean;
|
||||
}
|
||||
|
||||
export const DemoErrorScreen: React.FC<Props> = ({
|
||||
error,
|
||||
details,
|
||||
onRetry,
|
||||
isRetrying = false,
|
||||
}) => {
|
||||
return (
|
||||
<PublicLayout
|
||||
variant="centered"
|
||||
headerProps={{
|
||||
showThemeToggle: true,
|
||||
showAuthButtons: false,
|
||||
}}
|
||||
>
|
||||
<div className="max-w-2xl mx-auto p-8">
|
||||
<Card className="shadow-xl">
|
||||
<CardBody className="p-8 text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<AlertCircle className="w-20 h-20 text-[var(--color-error)]" />
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-bold text-[var(--color-error)] mb-3">
|
||||
Error en la Configuración del Demo
|
||||
</h1>
|
||||
|
||||
<p className="text-[var(--text-secondary)] mb-6 text-lg">
|
||||
{error}
|
||||
</p>
|
||||
|
||||
{details && details.length > 0 && (
|
||||
<div className="mb-6 p-4 bg-[var(--color-error)]/10 border border-[var(--color-error)] rounded-lg text-left">
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">
|
||||
Detalles del error:
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{details.map((detail, idx) => (
|
||||
<li key={idx} className="text-sm">
|
||||
<span className="font-medium text-[var(--text-primary)]">
|
||||
{detail.service}:
|
||||
</span>{' '}
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
{detail.error_message}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 max-w-md mx-auto mt-6">
|
||||
<Button
|
||||
onClick={onRetry}
|
||||
disabled={isRetrying}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
>
|
||||
<RefreshCw className={`w-5 h-5 mr-2 ${isRetrying ? 'animate-spin' : ''}`} />
|
||||
{isRetrying ? 'Reintentando...' : 'Reintentar Configuración'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => window.location.href = '/demo'}
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
>
|
||||
<Home className="w-5 h-5 mr-2" />
|
||||
Volver a la Página Demo
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => window.location.href = '/contact'}
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
>
|
||||
<HelpCircle className="w-5 h-5 mr-2" />
|
||||
Contactar Soporte
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<p className="text-xs text-[var(--text-tertiary)]">
|
||||
Si el problema persiste, por favor contacta a nuestro equipo de soporte.
|
||||
</p>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
132
frontend/src/components/demo/DemoProgressIndicator.tsx
Normal file
132
frontend/src/components/demo/DemoProgressIndicator.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import { Badge, ProgressBar } from '../ui';
|
||||
import { CheckCircle, XCircle, Loader2, Clock } from 'lucide-react';
|
||||
import { ServiceProgress } from '@/api/services/demo';
|
||||
|
||||
interface Props {
|
||||
progress: Record<string, ServiceProgress>;
|
||||
}
|
||||
|
||||
const SERVICE_LABELS: Record<string, string> = {
|
||||
tenant: 'Tenant Virtual',
|
||||
inventory: 'Inventario y Productos',
|
||||
recipes: 'Recetas',
|
||||
sales: 'Historial de Ventas',
|
||||
orders: 'Pedidos de Clientes',
|
||||
suppliers: 'Proveedores',
|
||||
production: 'Producción',
|
||||
forecasting: 'Pronósticos',
|
||||
};
|
||||
|
||||
const SERVICE_DESCRIPTIONS: Record<string, string> = {
|
||||
tenant: 'Creando tu entorno demo aislado',
|
||||
inventory: 'Cargando ingredientes, recetas y datos de stock',
|
||||
recipes: 'Configurando recetas y fórmulas',
|
||||
sales: 'Importando registros de ventas históricas',
|
||||
orders: 'Configurando pedidos de clientes',
|
||||
suppliers: 'Importando datos de proveedores',
|
||||
production: 'Configurando lotes de producción',
|
||||
forecasting: 'Preparando datos de pronósticos',
|
||||
};
|
||||
|
||||
export const DemoProgressIndicator: React.FC<Props> = ({ progress }) => {
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{Object.entries(progress).map(([serviceName, serviceProgress]) => (
|
||||
<div
|
||||
key={serviceName}
|
||||
className={`
|
||||
p-4 rounded-lg border-2 transition-all
|
||||
${
|
||||
serviceProgress.status === 'completed'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-500'
|
||||
: serviceProgress.status === 'failed'
|
||||
? 'bg-red-50 dark:bg-red-900/20 border-red-500'
|
||||
: serviceProgress.status === 'in_progress'
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500'
|
||||
: 'bg-gray-50 dark:bg-gray-800/20 border-gray-300 dark:border-gray-700'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<div className="flex-shrink-0">
|
||||
{getStatusIcon(serviceProgress.status)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)] truncate">
|
||||
{SERVICE_LABELS[serviceName] || serviceName}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--text-secondary)] truncate">
|
||||
{SERVICE_DESCRIPTIONS[serviceName] || 'Procesando...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={getStatusVariant(serviceProgress.status)}>
|
||||
{getStatusLabel(serviceProgress.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{serviceProgress.status === 'in_progress' && (
|
||||
<div className="my-2">
|
||||
<ProgressBar value={50} variant="primary" className="animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{serviceProgress.records_cloned > 0 && (
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-2">
|
||||
✓ {serviceProgress.records_cloned} registros clonados
|
||||
</p>
|
||||
)}
|
||||
|
||||
{serviceProgress.error && (
|
||||
<p className="text-xs text-[var(--color-error)] mt-2">
|
||||
Error: {serviceProgress.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getStatusIcon(status: ServiceProgress['status']): React.ReactNode {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <CheckCircle className="w-5 h-5 text-green-500" />;
|
||||
case 'failed':
|
||||
return <XCircle className="w-5 h-5 text-red-500" />;
|
||||
case 'in_progress':
|
||||
return <Loader2 className="w-5 h-5 text-blue-500 animate-spin" />;
|
||||
default:
|
||||
return <Clock className="w-5 h-5 text-gray-400" />;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusLabel(status: ServiceProgress['status']): string {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'Completado';
|
||||
case 'failed':
|
||||
return 'Fallido';
|
||||
case 'in_progress':
|
||||
return 'En Progreso';
|
||||
default:
|
||||
return 'Pendiente';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusVariant(
|
||||
status: ServiceProgress['status']
|
||||
): 'success' | 'error' | 'info' | 'default' {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'error';
|
||||
case 'in_progress':
|
||||
return 'info';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
@@ -211,9 +211,6 @@ export const AppShell = forwardRef<AppShellRef, AppShellProps>(({
|
||||
)}
|
||||
data-testid="app-shell"
|
||||
>
|
||||
{/* Demo Banner */}
|
||||
<DemoBanner />
|
||||
|
||||
{/* Header */}
|
||||
{shouldShowHeader && (
|
||||
<Header
|
||||
@@ -223,6 +220,9 @@ export const AppShell = forwardRef<AppShellRef, AppShellProps>(({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Demo Banner - appears below header */}
|
||||
<DemoBanner />
|
||||
|
||||
<div className="flex flex-1 relative">
|
||||
{/* Sidebar */}
|
||||
{shouldShowSidebar && (
|
||||
|
||||
@@ -2,19 +2,26 @@ import React, { useState, useEffect } from 'react';
|
||||
import { extendDemoSession, destroyDemoSession } from '../../../api/services/demo';
|
||||
import { apiClient } from '../../../api/client';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDemoTour, getTourState, trackTourEvent } from '../../../features/demo-onboarding';
|
||||
import { BookOpen, Clock, Sparkles, X } from 'lucide-react';
|
||||
|
||||
export const DemoBanner: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [isDemo, setIsDemo] = useState(false);
|
||||
const [expiresAt, setExpiresAt] = useState<string | null>(null);
|
||||
const { startTour, resumeTour, tourState } = useDemoTour();
|
||||
const [isDemo, setIsDemo] = useState(() => localStorage.getItem('demo_mode') === 'true');
|
||||
const [expiresAt, setExpiresAt] = useState<string | null>(() => localStorage.getItem('demo_expires_at'));
|
||||
const [timeRemaining, setTimeRemaining] = useState<string>('');
|
||||
const [canExtend, setCanExtend] = useState(true);
|
||||
const [extending, setExtending] = useState(false);
|
||||
const [showExitModal, setShowExitModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const demoMode = localStorage.getItem('demo_mode') === 'true';
|
||||
const expires = localStorage.getItem('demo_expires_at');
|
||||
|
||||
console.log('[DemoBanner] Demo mode from localStorage:', demoMode);
|
||||
console.log('[DemoBanner] Expires at:', expires);
|
||||
|
||||
setIsDemo(demoMode);
|
||||
setExpiresAt(expires);
|
||||
|
||||
@@ -43,6 +50,7 @@ export const DemoBanner: React.FC = () => {
|
||||
localStorage.removeItem('demo_session_id');
|
||||
localStorage.removeItem('demo_account_type');
|
||||
localStorage.removeItem('demo_expires_at');
|
||||
localStorage.removeItem('demo_tenant_id');
|
||||
apiClient.setDemoSessionId(null);
|
||||
navigate('/demo');
|
||||
};
|
||||
@@ -69,77 +77,172 @@ export const DemoBanner: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleEndSession = async () => {
|
||||
setShowExitModal(true);
|
||||
};
|
||||
|
||||
const confirmEndSession = async () => {
|
||||
const sessionId = apiClient.getDemoSessionId();
|
||||
if (!sessionId) return;
|
||||
|
||||
if (confirm('¿Estás seguro de que quieres terminar la sesión demo?')) {
|
||||
try {
|
||||
await destroyDemoSession({ session_id: sessionId });
|
||||
} catch (error) {
|
||||
console.error('Error destroying session:', error);
|
||||
} finally {
|
||||
handleExpiration();
|
||||
}
|
||||
try {
|
||||
await destroyDemoSession({ session_id: sessionId });
|
||||
} catch (error) {
|
||||
console.error('Error destroying session:', error);
|
||||
} finally {
|
||||
handleExpiration();
|
||||
}
|
||||
};
|
||||
|
||||
if (!isDemo) return null;
|
||||
const handleCreateAccount = () => {
|
||||
trackTourEvent({
|
||||
event: 'conversion_cta_clicked',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
navigate('/register?from=demo_banner');
|
||||
};
|
||||
|
||||
const handleStartTour = () => {
|
||||
if (tourState && tourState.currentStep > 0 && !tourState.completed) {
|
||||
resumeTour();
|
||||
} else {
|
||||
startTour();
|
||||
}
|
||||
};
|
||||
|
||||
if (!isDemo) {
|
||||
console.log('[DemoBanner] Not demo mode, returning null');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-r from-amber-500 to-orange-500 text-white px-4 py-2 shadow-md">
|
||||
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="font-medium">Modo Demo</span>
|
||||
<>
|
||||
<div
|
||||
data-tour="demo-banner"
|
||||
className="bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-lg sticky top-[var(--header-height)] z-[1100]"
|
||||
style={{ minHeight: '60px' }}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 py-3">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
|
||||
{/* Left section - Demo info */}
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
<span className="font-bold text-base">Sesión Demo Activa</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm bg-white/20 rounded-md px-3 py-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span className="font-mono font-semibold">{timeRemaining}</span>
|
||||
</div>
|
||||
|
||||
{tourState && !tourState.completed && tourState.currentStep > 0 && (
|
||||
<div className="hidden md:flex items-center gap-2 text-sm bg-white/20 rounded-md px-3 py-1">
|
||||
<span>Tutorial pausado en paso {tourState.currentStep}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right section - Actions */}
|
||||
<div data-tour="demo-banner-actions" className="flex items-center gap-2 flex-wrap">
|
||||
{/* Tour button */}
|
||||
<button
|
||||
onClick={handleStartTour}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-white text-amber-600 rounded-lg text-sm font-semibold hover:bg-amber-50 transition-all shadow-sm hover:shadow-md"
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">
|
||||
{tourState && tourState.currentStep > 0 && !tourState.completed
|
||||
? 'Continuar Tutorial'
|
||||
: 'Ver Tutorial'}
|
||||
</span>
|
||||
<span className="sm:hidden">Tutorial</span>
|
||||
</button>
|
||||
|
||||
{/* Create account CTA */}
|
||||
<button
|
||||
onClick={handleCreateAccount}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-white text-orange-600 rounded-lg text-sm font-bold hover:bg-orange-50 transition-all shadow-sm hover:shadow-md animate-pulse hover:animate-none"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="hidden lg:inline">¡Crear Cuenta Gratis!</span>
|
||||
<span className="lg:hidden">Crear Cuenta</span>
|
||||
</button>
|
||||
|
||||
{/* Extend session */}
|
||||
{canExtend && (
|
||||
<button
|
||||
onClick={handleExtendSession}
|
||||
disabled={extending}
|
||||
className="px-3 py-2 bg-white/20 hover:bg-white/30 rounded-lg text-sm font-medium transition-colors disabled:opacity-50 hidden sm:block"
|
||||
>
|
||||
{extending ? 'Extendiendo...' : '+30 min'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* End session */}
|
||||
<button
|
||||
onClick={handleEndSession}
|
||||
className="px-3 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
<span className="hidden sm:inline">Salir</span>
|
||||
<X className="w-4 h-4 sm:hidden" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center text-sm">
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Tiempo restante: <span className="font-mono ml-1">{timeRemaining}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{canExtend && (
|
||||
<button
|
||||
onClick={handleExtendSession}
|
||||
disabled={extending}
|
||||
className="px-3 py-1 bg-white/20 hover:bg-white/30 rounded-md text-sm font-medium transition-colors disabled:opacity-50"
|
||||
>
|
||||
{extending ? 'Extendiendo...' : '+30 min'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleEndSession}
|
||||
className="px-3 py-1 bg-white/20 hover:bg-white/30 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
Terminar Demo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exit confirmation modal */}
|
||||
{showExitModal && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[9999] flex items-center justify-center p-4">
|
||||
<div className="bg-[var(--bg-primary)] rounded-2xl shadow-2xl max-w-md w-full p-6 border border-[var(--border-default)]">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="p-3 bg-amber-100 dark:bg-amber-900/30 rounded-xl">
|
||||
<Sparkles className="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-2">
|
||||
¿Seguro que quieres salir?
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] text-sm leading-relaxed">
|
||||
Aún te quedan <span className="font-bold text-amber-600">{timeRemaining}</span> de sesión demo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/30 rounded-xl p-4 mb-6 border border-amber-200 dark:border-amber-800">
|
||||
<p className="text-sm font-semibold text-[var(--text-primary)] mb-3">
|
||||
¿Te gusta lo que ves?
|
||||
</p>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-4 leading-relaxed">
|
||||
Crea una cuenta <span className="font-bold">gratuita</span> para acceder a todas las funcionalidades sin límites de tiempo y guardar tus datos de forma permanente.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleCreateAccount}
|
||||
className="w-full py-3 bg-gradient-to-r from-amber-500 to-orange-500 text-white rounded-lg font-bold hover:from-amber-600 hover:to-orange-600 transition-all shadow-md hover:shadow-lg"
|
||||
>
|
||||
Crear Mi Cuenta Gratis
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setShowExitModal(false)}
|
||||
className="flex-1 px-4 py-2.5 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)] rounded-lg font-semibold transition-colors border border-[var(--border-default)]"
|
||||
>
|
||||
Seguir en Demo
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmEndSession}
|
||||
className="flex-1 px-4 py-2.5 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/30 text-red-600 dark:text-red-400 rounded-lg font-semibold transition-colors border border-red-200 dark:border-red-800"
|
||||
>
|
||||
Salir de Demo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -154,15 +154,17 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
{/* Left section */}
|
||||
<div className="flex items-center gap-2 sm:gap-4 flex-1 min-w-0 h-full">
|
||||
{/* Mobile menu button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onMenuClick}
|
||||
className="lg:hidden w-10 h-10 p-0 flex items-center justify-center hover:bg-[var(--bg-secondary)] active:scale-95 transition-all duration-150"
|
||||
aria-label={t('common:header.open_menu', 'Abrir menú de navegación')}
|
||||
>
|
||||
<Menu className="h-5 w-5 text-[var(--text-primary)]" />
|
||||
</Button>
|
||||
<div data-tour="sidebar-menu-toggle">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onMenuClick}
|
||||
className="lg:hidden w-10 h-10 p-0 flex items-center justify-center hover:bg-[var(--bg-secondary)] active:scale-95 transition-all duration-150"
|
||||
aria-label={t('common:header.open_menu', 'Abrir menú de navegación')}
|
||||
>
|
||||
<Menu className="h-5 w-5 text-[var(--text-primary)]" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-shrink-0">
|
||||
@@ -185,7 +187,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
|
||||
{/* Tenant Switcher - Desktop */}
|
||||
{hasAccess && (
|
||||
<div className="hidden md:block mx-2 lg:mx-4 flex-shrink-0">
|
||||
<div className="hidden md:block mx-2 lg:mx-4 flex-shrink-0" data-tour="header-tenant-selector">
|
||||
<TenantSwitcher
|
||||
showLabel={true}
|
||||
className="min-w-[160px] max-w-[220px] lg:min-w-[200px] lg:max-w-[280px]"
|
||||
|
||||
@@ -516,6 +516,16 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
const isHovered = hoveredItem === item.id;
|
||||
const ItemIcon = item.icon;
|
||||
|
||||
// Add tour data attributes for main navigation sections
|
||||
const getTourAttribute = (itemPath: string) => {
|
||||
if (itemPath === '/app/database') return 'sidebar-database';
|
||||
if (itemPath === '/app/operations') return 'sidebar-operations';
|
||||
if (itemPath === '/app/analytics') return 'sidebar-analytics';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const tourAttr = getTourAttribute(item.path);
|
||||
|
||||
const itemContent = (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -635,7 +645,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={item.id} className="relative">
|
||||
<li key={item.id} className="relative" data-tour={tourAttr}>
|
||||
{isCollapsed && !hasChildren && ItemIcon ? (
|
||||
<Tooltip content={item.label} side="right">
|
||||
{button}
|
||||
|
||||
Reference in New Issue
Block a user