Improve public pages

This commit is contained in:
Urtzi Alfaro
2025-10-17 18:14:28 +02:00
parent d4060962e4
commit 7e089b80cf
46 changed files with 5734 additions and 1084 deletions

View File

@@ -1,18 +1,21 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import React, { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { PublicLayout } from '../../components/layout';
import { Button } from '../../components/ui';
import { getDemoAccounts, createDemoSession, DemoAccount } from '../../api/services/demo';
import { getDemoAccounts, createDemoSession, DemoAccount, demoSessionAPI } from '../../api/services/demo';
import { apiClient } from '../../api/client';
import { Check, Clock, Shield, Play, Zap, ArrowRight, Store, Factory } from 'lucide-react';
import { Check, Clock, Shield, Play, Zap, ArrowRight, Store, Factory, Loader2 } from 'lucide-react';
import { markTourAsStartPending } from '../../features/demo-onboarding';
const POLL_INTERVAL_MS = 1500; // Poll every 1.5 seconds
export const DemoPage: React.FC = () => {
const navigate = useNavigate();
const [demoAccounts, setDemoAccounts] = useState<DemoAccount[]>([]);
const [loading, setLoading] = useState(true);
const [creatingSession, setCreatingSession] = useState(false);
const [error, setError] = useState<string | null>(null);
const [progressPercentage, setProgressPercentage] = useState(0);
const [estimatedTime, setEstimatedTime] = useState(5);
useEffect(() => {
const fetchDemoAccounts = async () => {
@@ -30,9 +33,63 @@ export const DemoPage: React.FC = () => {
fetchDemoAccounts();
}, []);
const pollStatus = useCallback(async (sessionId: string) => {
try {
const statusData = await demoSessionAPI.getSessionStatus(sessionId);
// Calculate progress - ALWAYS update, even if no progress data yet
if (statusData.progress && Object.keys(statusData.progress).length > 0) {
const services = Object.values(statusData.progress);
const totalServices = services.length;
if (totalServices > 0) {
const completedServices = services.filter(
(s) => s.status === 'completed' || s.status === 'failed'
).length;
const percentage = Math.round((completedServices / totalServices) * 100);
setProgressPercentage(percentage);
// Estimate remaining time
const remainingServices = totalServices - completedServices;
setEstimatedTime(Math.max(remainingServices * 2, 1));
} else {
// No services yet, show minimal progress
setProgressPercentage(5);
}
} else {
// No progress data yet, show initial state
setProgressPercentage(10);
}
// Check if ready to redirect
const hasUsableData = statusData.total_records_cloned > 100;
const shouldRedirect =
statusData.status === 'ready' ||
(statusData.status === 'partial' && hasUsableData) ||
(statusData.status === 'failed' && hasUsableData);
if (shouldRedirect) {
// Show 100% before redirect
setProgressPercentage(100);
// Small delay for smooth transition
setTimeout(() => {
window.location.href = `/app/dashboard?session=${sessionId}`;
}, 300);
return true; // Stop polling
}
return false; // Continue polling
} catch (err) {
console.error('Error polling session status:', err);
return false;
}
}, []);
const handleStartDemo = async (accountType: string) => {
setCreatingSession(true);
setError(null);
setProgressPercentage(0);
setEstimatedTime(6);
try {
const session = await createDemoSession({
@@ -44,8 +101,7 @@ export const DemoPage: React.FC = () => {
// Store session ID in API client
apiClient.setDemoSessionId(session.session_id);
// **CRITICAL FIX: Set the virtual tenant ID in API client**
// This ensures all API requests include the correct tenant context
// Set the virtual tenant ID in API client
apiClient.setTenantId(session.virtual_tenant_id);
console.log('✅ Set API client tenant ID:', session.virtual_tenant_id);
@@ -56,38 +112,48 @@ export const DemoPage: React.FC = () => {
localStorage.setItem('demo_expires_at', session.expires_at);
localStorage.setItem('demo_tenant_id', session.virtual_tenant_id);
// **CRITICAL FIX: Initialize tenant store with demo tenant**
// This ensures useTenantId() returns the correct virtual tenant ID
const { useTenantStore } = await import('../../stores/tenant.store');
const demoTenant = {
id: session.virtual_tenant_id,
name: session.demo_config?.name || `Demo ${accountType}`,
business_type: accountType === 'individual_bakery' ? 'bakery' : 'central_baker',
business_model: accountType,
address: session.demo_config?.address || 'Demo Address',
city: session.demo_config?.city || 'Madrid',
postal_code: '28001',
phone: null,
is_active: true,
subscription_tier: 'demo',
ml_model_trained: false,
last_training_date: null,
owner_id: 'demo-user',
created_at: new Date().toISOString(),
};
// Start polling IMMEDIATELY in parallel with other setup
const pollInterval = setInterval(async () => {
const shouldStop = await pollStatus(session.session_id);
if (shouldStop) {
clearInterval(pollInterval);
}
}, POLL_INTERVAL_MS);
useTenantStore.getState().setCurrentTenant(demoTenant);
console.log('✅ Initialized tenant store with demo tenant:', demoTenant);
// Initialize tenant store and other setup in parallel (non-blocking)
Promise.all([
import('../../stores/tenant.store').then(({ useTenantStore }) => {
const demoTenant = {
id: session.virtual_tenant_id,
name: session.demo_config?.name || `Demo ${accountType}`,
business_type: accountType === 'individual_bakery' ? 'bakery' : 'central_baker',
business_model: accountType,
address: session.demo_config?.address || 'Demo Address',
city: session.demo_config?.city || 'Madrid',
postal_code: '28001',
phone: null,
is_active: true,
subscription_tier: 'demo',
ml_model_trained: false,
last_training_date: null,
owner_id: 'demo-user',
created_at: new Date().toISOString(),
};
useTenantStore.getState().setCurrentTenant(demoTenant);
console.log('✅ Initialized tenant store with demo tenant:', demoTenant);
}),
// Mark tour to start automatically
Promise.resolve(markTourAsStartPending()),
]).catch(err => console.error('Error initializing tenant store:', err));
// Mark tour to start automatically
markTourAsStartPending();
// Navigate to setup page to wait for data cloning
navigate(`/demo/setup?session=${session.session_id}`);
// Initial poll (don't wait for tenant store)
const shouldStop = await pollStatus(session.session_id);
if (shouldStop) {
clearInterval(pollInterval);
}
} catch (err: any) {
setError(err?.message || 'Error al crear sesión demo');
console.error('Error creating demo session:', err);
} finally {
setCreatingSession(false);
}
};
@@ -247,20 +313,8 @@ export const DemoPage: React.FC = () => {
size="lg"
className="w-full bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
>
{creatingSession ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Creando sesión...
</span>
) : (
<>
<Play className="mr-2 w-5 h-5" />
Probar Demo Ahora
</>
)}
<Play className="mr-2 w-5 h-5" />
Probar Demo Ahora
</Button>
</div>
</div>
@@ -283,6 +337,68 @@ export const DemoPage: React.FC = () => {
</div>
</div>
</section>
{/* Loading Modal Overlay */}
{creatingSession && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-[var(--bg-primary)] rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 border border-[var(--border-default)]">
<div className="text-center">
{/* Animated loader */}
<div className="mb-6 flex justify-center">
<div className="relative w-20 h-20">
<Loader2 className="w-20 h-20 text-[var(--color-primary)] animate-spin" />
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xl font-bold text-[var(--color-primary)]">
{Math.min(progressPercentage, 100)}%
</span>
</div>
</div>
</div>
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-2">
{progressPercentage >= 100 ? '¡Listo! Redirigiendo...' : 'Preparando tu Demo'}
</h2>
<p className="text-[var(--text-secondary)] mb-6">
{progressPercentage >= 100
? 'Tu entorno está listo. Accediendo al dashboard...'
: 'Configurando tu entorno personalizado con datos de muestra...'}
</p>
{/* Progress bar */}
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3 mb-4 overflow-hidden">
<div
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] h-full rounded-full transition-all duration-500 ease-out"
style={{ width: `${Math.min(progressPercentage, 100)}%` }}
/>
</div>
{/* Estimated time - Only show if not complete */}
{progressPercentage < 100 && (
<div className="flex items-center justify-center text-sm text-[var(--text-tertiary)] mb-4">
<Clock className="w-4 h-4 mr-2" />
<span>Tiempo estimado: ~{estimatedTime}s</span>
</div>
)}
{/* Tips while loading */}
{progressPercentage < 100 && (
<div className="mt-2 p-4 bg-[var(--color-primary)]/5 rounded-lg border border-[var(--color-primary)]/20">
<p className="text-xs text-[var(--text-secondary)] italic">
💡 Tip: La demo incluye datos reales de panaderías españolas para que puedas explorar todas las funcionalidades
</p>
</div>
)}
{/* Error message if any */}
{error && (
<div className="mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 rounded-lg text-sm">
{error}
</div>
)}
</div>
</div>
</div>
)}
</PublicLayout>
);
};