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

@@ -0,0 +1,76 @@
/**
* Pilot Banner Component
* Displays when pilot=true URL parameter is detected
* Shows 3-month free trial information and PILOT2025 coupon code
*/
import React from 'react';
import { Star, Check, Award } from 'lucide-react';
import { Card } from '../../ui';
interface PilotBannerProps {
couponCode: string;
trialMonths: number;
className?: string;
}
export const PilotBanner: React.FC<PilotBannerProps> = ({
couponCode,
trialMonths,
className = ''
}) => {
return (
<Card className={`p-6 border-2 border-amber-500 dark:border-amber-600 bg-gradient-to-r from-amber-50 via-orange-50 to-amber-50 dark:from-amber-900/20 dark:via-orange-900/20 dark:to-amber-900/20 ${className}`}>
<div className="flex items-start gap-4">
{/* Icon */}
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center shadow-lg">
<Award className="w-6 h-6 text-white" />
</div>
{/* Content */}
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Star className="w-5 h-5 text-amber-600 dark:text-amber-400 fill-amber-600 dark:fill-amber-400" />
<h3 className="text-lg font-bold text-amber-900 dark:text-amber-100">
¡Programa Piloto Activado!
</h3>
<Star className="w-5 h-5 text-amber-600 dark:text-amber-400 fill-amber-600 dark:fill-amber-400" />
</div>
<p className="text-sm text-amber-800 dark:text-amber-200 mb-3">
Has sido seleccionado para nuestro programa piloto exclusivo.
Disfruta de <strong>{trialMonths} meses completamente gratis</strong> como uno de nuestros primeros 20 clientes.
</p>
<div className="flex flex-wrap gap-3">
<div className="flex items-center gap-2 bg-white/60 dark:bg-gray-800/60 px-3 py-1.5 rounded-lg border border-amber-300 dark:border-amber-700">
<Check className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
Cupón: <span className="font-mono font-bold text-amber-700 dark:text-amber-300">{couponCode}</span>
</span>
</div>
<div className="flex items-center gap-2 bg-white/60 dark:bg-gray-800/60 px-3 py-1.5 rounded-lg border border-amber-300 dark:border-amber-700">
<Check className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
{trialMonths} meses gratis
</span>
</div>
<div className="flex items-center gap-2 bg-white/60 dark:bg-gray-800/60 px-3 py-1.5 rounded-lg border border-amber-300 dark:border-amber-700">
<Check className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
40% descuento de por vida
</span>
</div>
</div>
<p className="text-xs text-amber-700 dark:text-amber-300 mt-3 italic">
El cupón se aplicará automáticamente durante el registro. Tarjeta requerida para validación, sin cargo inmediato.
</p>
</div>
</div>
</Card>
);
};
export default PilotBanner;

View File

@@ -6,9 +6,11 @@ import { useAuthActions, useAuthLoading, useAuthError } from '../../../stores/au
import { useToast } from '../../../hooks/ui/useToast';
import { SubscriptionSelection } from './SubscriptionSelection';
import PaymentForm from './PaymentForm';
import PilotBanner from './PilotBanner';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import { CheckCircle } from 'lucide-react';
import { usePilotDetection } from '../../../hooks/usePilotDetection';
// Initialize Stripe - In production, use environment variable
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_51234567890123456789012345678901234567890123456789012345678901234567890123456789012345');
@@ -47,20 +49,23 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
marketingConsent: false,
analyticsConsent: false
});
const [errors, setErrors] = useState<Partial<SimpleUserRegistration>>({});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const { register } = useAuthActions();
const isLoading = useAuthLoading();
const error = useAuthError();
const { success: showSuccessToast, error: showErrorToast } = useToast();
// Detect pilot program participation
const { isPilot, couponCode, trialMonths } = usePilotDetection();
// Multi-step form state
const [currentStep, setCurrentStep] = useState<RegistrationStep>('basic_info');
const [currentStep, setCurrentStep] = useState<RegistrationStep>('basic_info');
const [selectedPlan, setSelectedPlan] = useState<string>('starter');
const [useTrial, setUseTrial] = useState<boolean>(false);
const [useTrial, setUseTrial] = useState<boolean>(isPilot); // Auto-enable trial for pilot customers
const [bypassPayment, setBypassPayment] = useState<boolean>(false);
// Helper function to determine password match status
@@ -139,6 +144,8 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
subscription_plan: selectedPlan,
use_trial: useTrial,
payment_method_id: paymentMethodId,
// Include coupon code if pilot customer
coupon_code: isPilot ? couponCode : undefined,
// Include consent data
terms_accepted: formData.acceptTerms,
privacy_accepted: formData.acceptTerms,
@@ -148,7 +155,11 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
await register(registrationData);
showSuccessToast(t('auth:register.registering', '¡Bienvenido! Tu cuenta ha sido creada correctamente.'), {
const successMessage = isPilot
? '¡Bienvenido al programa piloto! Tu cuenta ha sido creada con 3 meses gratis.'
: '¡Bienvenido! Tu cuenta ha sido creada correctamente.';
showSuccessToast(t('auth:register.registering', successMessage), {
title: t('auth:alerts.success_create', 'Cuenta creada exitosamente')
});
onSuccess?.();
@@ -157,7 +168,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
title: t('auth:alerts.error_create', 'Error al crear la cuenta')
});
}
};
};
const handlePaymentSuccess = () => {
handleRegistrationSubmit(); // In a real app, you would pass the payment method ID
@@ -486,10 +497,14 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
</p>
</div>
{isPilot && couponCode && (
<PilotBanner couponCode={couponCode} trialMonths={trialMonths} />
)}
<SubscriptionSelection
selectedPlan={selectedPlan}
onPlanSelect={setSelectedPlan}
showTrialOption={true}
showTrialOption={!isPilot}
onTrialSelect={setUseTrial}
trialSelected={useTrial}
/>

View File

@@ -143,8 +143,8 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
return translations[feature] || feature.replace(/_/g, ' ');
};
// Get trial days from the selected plan (default to 14 if not available)
const trialDays = availablePlans.plans[selectedPlan]?.trial_days || 14;
// Get trial days from the selected plan (default to 90 for pilot customers)
const trialDays = 90; // 3 months for pilot customers
return (
<div className={`space-y-4 ${className}`}>
@@ -160,7 +160,7 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
{t('auth:subscription.trial_title', 'Prueba gratuita')}
</h3>
<p className="text-sm text-text-secondary">
{t('auth:subscription.trial_description', `Obtén ${trialDays} días de prueba gratuita - sin tarjeta de crédito requerida`)}
{t('auth:subscription.trial_description', `Obtén 3 meses de prueba gratuita - tarjeta requerida para validación`)}
</p>
</div>
</div>
@@ -218,7 +218,7 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
{metadata.trial_days > 0 && (
<Badge variant="success" className="text-xs px-2 py-0.5">
<Zap className="w-3 h-3 mr-1" />
{metadata.trial_days} días gratis
3 meses gratis
</Badge>
)}
</div>