Add subcription feature 5

This commit is contained in:
Urtzi Alfaro
2026-01-16 09:55:54 +01:00
parent 483a9f64cd
commit 6b43116efd
51 changed files with 1428 additions and 312 deletions

View File

@@ -33,13 +33,15 @@ const SubscriptionStatusBanner: React.FC<{
isTrial?: boolean;
trialEndsAt?: string;
trialDaysRemaining?: number;
}> = ({ planName, status, nextBilling, cost, onUpgrade, onManagePayment, showUpgradeButton, isTrial, trialEndsAt, trialDaysRemaining }) => {
isPilot?: boolean;
}> = ({ planName, status, nextBilling, cost, onUpgrade, onManagePayment, showUpgradeButton, isTrial, trialEndsAt, trialDaysRemaining, isPilot }) => {
// Determine if we should show trial information
const showTrialInfo = isTrial && trialEndsAt;
const showPilotInfo = isPilot;
return (
<Card className={`p-6 mb-6 ${showTrialInfo
<Card className={`p-6 mb-6 ${showTrialInfo || showPilotInfo
? 'bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-900/30 dark:to-cyan-900/30 border-2 border-blue-300 dark:border-blue-600'
: 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 border-2 border-blue-200 dark:border-blue-700'}`}
>
@@ -47,27 +49,29 @@ const SubscriptionStatusBanner: React.FC<{
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)] flex items-center gap-2">
<Crown className="w-5 h-5 text-yellow-500" />
{planName} {showTrialInfo && <span className="text-blue-600 dark:text-blue-400">(Prueba Gratis)</span>}
{planName} {showPilotInfo && <span className="text-blue-600 dark:text-blue-400">(Programa Piloto)</span>}
{!showPilotInfo && showTrialInfo && <span className="text-blue-600 dark:text-blue-400">(Prueba Gratis)</span>}
</h3>
<Badge variant={showTrialInfo ? 'info' : status === 'active' ? 'success' : 'default'} className="mt-2">
{showTrialInfo ? `En Prueba (${trialDaysRemaining} días)` : status === 'active' ? 'Activo' : status}
<Badge variant={showPilotInfo ? 'info' : showTrialInfo ? 'info' : status === 'active' ? 'success' : 'default'} className="mt-2">
{showPilotInfo ? 'Piloto Activo' : showTrialInfo ? `En Prueba (${trialDaysRemaining} días)` : status === 'active' ? 'Activo' : status}
</Badge>
</div>
<div className="text-center">
<p className="text-sm text-[var(--text-secondary)]">{showTrialInfo ? 'Fin de la Prueba' : 'Próxima Facturación'}</p>
<p className="font-semibold text-[var(--text-primary)]">{showTrialInfo ? trialEndsAt : nextBilling}</p>
<p className="text-sm text-[var(--text-secondary)]">{showPilotInfo ? 'Fin del Piloto' : showTrialInfo ? 'Fin de la Prueba' : 'Próxima Facturación'}</p>
<p className="font-semibold text-[var(--text-primary)]">{showPilotInfo ? trialEndsAt : showTrialInfo ? trialEndsAt : nextBilling}</p>
</div>
<div className="text-center">
<p className="text-sm text-[var(--text-secondary)]">Costo Mensual</p>
<p className="font-semibold text-[var(--text-primary)] text-lg">
{cost} {showTrialInfo && <span className="text-xs text-blue-600 dark:text-blue-400">(después de la prueba)</span>}
{cost} {showPilotInfo && <span className="text-xs text-blue-600 dark:text-blue-400">(después del periodo piloto)</span>}
{!showPilotInfo && showTrialInfo && <span className="text-xs text-blue-600 dark:text-blue-400">(después de la prueba)</span>}
</p>
</div>
<div className="flex flex-col gap-2">
{showTrialInfo ? (
{showPilotInfo || showTrialInfo ? (
<>
<Button onClick={onUpgrade} variant="primary" size="sm" className="w-full bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700">
<ArrowRight className="w-4 h-4 mr-2" />
@@ -94,9 +98,9 @@ const SubscriptionStatusBanner: React.FC<{
)}
</div>
</div>
{/* Trial Countdown Banner */}
{showTrialInfo && (
{/* Pilot/Trial Countdown Banner */}
{(showPilotInfo || showTrialInfo) && (
<div className="mt-4 p-4 bg-gradient-to-r from-blue-600/10 to-cyan-600/10 border border-blue-300/50 rounded-lg">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/20 rounded-lg border border-blue-500/30 flex-shrink-0">
@@ -104,7 +108,10 @@ const SubscriptionStatusBanner: React.FC<{
</div>
<div className="flex-1">
<p className="text-sm font-medium text-blue-700 dark:text-blue-300">
¡Disfruta de tu periodo de prueba! Tu acceso gratuito termina en <strong>{trialDaysRemaining} días</strong>.
{showPilotInfo
? `¡Eres parte de nuestro programa piloto! Tu acceso gratuito termina en <strong>${trialDaysRemaining} días</strong>.`
: `¡Disfruta de tu periodo de prueba! Tu acceso gratuito termina en <strong>${trialDaysRemaining} días</strong>.`
}
</p>
</div>
<Button onClick={onUpgrade} variant="ghost" size="sm" className="text-blue-600 dark:text-blue-400 hover:bg-blue-500/10">
@@ -632,6 +639,15 @@ const SubscriptionPageRedesign: React.FC = () => {
// Check if there are any high usage metrics
const hasHighUsageMetrics = forecastUsage && forecastUsage.highUsageMetrics && forecastUsage.highUsageMetrics.length > 0;
// Determine if this is a pilot subscription based on characteristics
// Pilot subscriptions have extended trial periods (typically 90 days from PILOT2025 coupon)
// compared to regular trials (typically 14 days), so we check for longer trial periods
const isPilotSubscription = usageSummary.status === 'trialing' &&
usageSummary.trial_ends_at &&
new Date(usageSummary.trial_ends_at) > new Date() &&
// Check if trial period is longer than typical trial (e.g., > 60 days indicates pilot)
(new Date(usageSummary.trial_ends_at).getTime() - new Date().getTime()) > (60 * 24 * 60 * 60 * 1000); // 60+ days
return (
<div className="p-6 space-y-6">
<PageHeader
@@ -643,20 +659,21 @@ const SubscriptionPageRedesign: React.FC = () => {
<SubscriptionStatusBanner
planName={usageSummary.plan.charAt(0).toUpperCase() + usageSummary.plan.slice(1)}
status={usageSummary.status}
nextBilling={new Date(usageSummary.next_billing_date).toLocaleDateString('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
nextBilling={new Date(usageSummary.next_billing_date).toLocaleDateString('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})}
cost={subscriptionService.formatPrice(usageSummary.monthly_price)}
onUpgrade={() => setShowPlans(true)}
onManagePayment={() => setPaymentMethodModalOpen(true)}
showUpgradeButton={usageSummary.plan !== 'enterprise'}
isTrial={usageSummary.status === 'trialing'}
trialEndsAt={usageSummary.trial_ends_at ? new Date(usageSummary.trial_ends_at).toLocaleDateString('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
isTrial={usageSummary.status === 'trialing' || isPilotSubscription}
isPilot={isPilotSubscription}
trialEndsAt={usageSummary.trial_ends_at ? new Date(usageSummary.trial_ends_at).toLocaleDateString('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
}) : undefined}
trialDaysRemaining={usageSummary.trial_ends_at ? Math.max(0, Math.ceil((new Date(usageSummary.trial_ends_at).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24))) : undefined}
/>

View File

@@ -0,0 +1,197 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button, Input, Card } from '../../components/ui';
import { useRequestPasswordReset } from '../../api/hooks/auth';
import { showToast } from '../../utils/toast';
import { validateEmail } from '../../utils/validation';
import { PublicLayout } from '../../components/layout';
const ForgotPasswordPage: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [errors, setErrors] = useState<{ email?: string }>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const { mutate: requestPasswordReset } = useRequestPasswordReset({
onSuccess: () => {
showToast.success(
'Si una cuenta con este email existe, recibirás un enlace de recuperación.',
{ title: 'Instrucciones enviadas' }
);
setIsSubmitted(true);
},
onError: (error: any) => {
const errorMessage = error.response?.data?.detail ||
'Hubo un problema al enviar las instrucciones de recuperación.';
showToast.error(errorMessage, { title: 'Error' });
},
onSettled: () => {
setIsSubmitting(false);
}
});
const validateForm = (): boolean => {
const newErrors: { email?: string } = {};
const emailValidation = validateEmail(email);
if (!emailValidation.isValid) {
newErrors.email = t('auth:validation.email_invalid', 'Por favor, ingrese un email válido');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
requestPasswordReset(email);
};
const handleGoBackToLogin = () => {
navigate('/login');
};
if (isSubmitted) {
return (
<PublicLayout
variant="centered"
maxWidth="md"
headerProps={{
showThemeToggle: true,
showAuthButtons: false,
showLanguageSelector: true,
variant: "minimal"
}}
>
<Card className="p-8 w-full max-w-md mx-auto" role="main">
<div className="text-center mb-8">
<div className="mx-auto bg-color-success/10 p-3 rounded-full w-16 h-16 flex items-center justify-center mb-4">
<svg
className="w-8 h-8 text-color-success"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h1 className="text-2xl font-bold text-text-primary mb-2">
{t('auth:forgot_password.submitted_title', '¡Instrucciones Enviadas!')}
</h1>
<p className="text-text-secondary">
{t('auth:forgot_password.submitted_message', 'Hemos enviado un enlace de recuperación a tu correo electrónico.')}
</p>
</div>
<div className="space-y-4">
<Button
variant="primary"
size="lg"
isFullWidth
onClick={handleGoBackToLogin}
className="mt-4"
>
Volver al Inicio de Sesión
</Button>
</div>
</Card>
</PublicLayout>
);
}
return (
<PublicLayout
variant="centered"
maxWidth="md"
headerProps={{
showThemeToggle: true,
showAuthButtons: false,
showLanguageSelector: true,
variant: "minimal"
}}
>
<Card className="p-8 w-full max-w-md mx-auto" role="main">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-text-primary mb-2">
{t('auth:forgot_password.title', '¿Olvidaste tu contraseña?')}
</h1>
<p className="text-text-secondary">
{t('auth:forgot_password.subtitle', 'Ingresa tu correo electrónico y te enviaremos un enlace para restablecer tu contraseña.')}
</p>
</div>
<form
onSubmit={handleSubmit}
className="space-y-6"
noValidate
aria-label={t('auth:forgot_password.title', 'Formulario de recuperación de contraseña')}
>
<div>
<Input
type="email"
label={t('auth:login.email', 'Correo Electrónico')}
placeholder="tu.email@panaderia.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={errors.email}
disabled={isSubmitting}
autoComplete="email"
required
aria-describedby={errors.email ? 'email-error' : undefined}
leftIcon={
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
</svg>
}
/>
{errors.email && (
<p id="email-error" className="text-color-error text-sm mt-1" role="alert">
{errors.email}
</p>
)}
</div>
<Button
type="submit"
variant="primary"
size="lg"
isFullWidth
isLoading={isSubmitting}
loadingText="Enviando instrucciones..."
disabled={isSubmitting}
>
{isSubmitting ? 'Enviando...' : 'Enviar Instrucciones'}
</Button>
</form>
<div className="mt-6 text-center">
<button
type="button"
onClick={handleGoBackToLogin}
className="text-sm text-color-primary hover:text-color-primary-dark transition-colors underline-offset-2 hover:underline focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-offset-2 rounded"
>
Volver al Inicio de Sesión
</button>
</div>
</Card>
</PublicLayout>
);
};
export default ForgotPasswordPage;

View File

@@ -30,8 +30,12 @@ const LoginPage: React.FC = () => {
navigate('/register');
};
const handleForgotPasswordClick = () => {
navigate('/forgot-password');
};
return (
<PublicLayout
<PublicLayout
variant="centered"
maxWidth="md"
headerProps={{
@@ -44,6 +48,7 @@ const LoginPage: React.FC = () => {
<LoginForm
onSuccess={handleLoginSuccess}
onRegisterClick={handleRegisterClick}
onForgotPasswordClick={handleForgotPasswordClick}
className="mx-auto"
/>
</PublicLayout>

View File

@@ -0,0 +1,313 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button, Input, Card } from '../../components/ui';
import { useResetPasswordWithToken } from '../../api/hooks/auth';
import { showToast } from '../../utils/toast';
import { PublicLayout } from '../../components/layout';
const ResetPasswordPage: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [errors, setErrors] = useState<{ password?: string; confirmPassword?: string }>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const token = searchParams.get('token');
useEffect(() => {
if (!token) {
navigate('/forgot-password');
showToast.error('Token de restablecimiento inválido o ausente.', { title: 'Error' });
}
}, [token, navigate]);
const { mutate: resetPasswordWithToken } = useResetPasswordWithToken({
onSuccess: () => {
showToast.success('Tu contraseña ha sido restablecida exitosamente.', {
title: '¡Contraseña Actualizada!'
});
setIsSubmitted(true);
},
onError: (error: any) => {
const errorMessage = error.response?.data?.detail ||
'Hubo un problema al restablecer tu contraseña.';
showToast.error(errorMessage, { title: 'Error' });
},
onSettled: () => {
setIsSubmitting(false);
}
});
const validateForm = (): boolean => {
const newErrors: { password?: string; confirmPassword?: string } = {};
if (!password) {
newErrors.password = 'La contraseña es requerida';
} else if (password.length < 8) {
newErrors.password = 'La contraseña debe tener al menos 8 caracteres';
} else if (!/(?=.*[a-z])/.test(password)) {
newErrors.password = 'La contraseña debe contener al menos una letra minúscula';
} else if (!/(?=.*[A-Z])/.test(password)) {
newErrors.password = 'La contraseña debe contener al menos una letra mayúscula';
} else if (!/(?=.*\d)/.test(password)) {
newErrors.password = 'La contraseña debe contener al menos un número';
}
if (!confirmPassword) {
newErrors.confirmPassword = 'Confirma tu nueva contraseña';
} else if (password !== confirmPassword) {
newErrors.confirmPassword = 'Las contraseñas no coinciden';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm() || !token) {
return;
}
setIsSubmitting(true);
resetPasswordWithToken({ token, newPassword: password });
};
const handleGoToLogin = () => {
navigate('/login');
};
if (isSubmitted) {
return (
<PublicLayout
variant="centered"
maxWidth="md"
headerProps={{
showThemeToggle: true,
showAuthButtons: false,
showLanguageSelector: true,
variant: "minimal"
}}
>
<Card className="p-8 w-full max-w-md mx-auto" role="main">
<div className="text-center mb-8">
<div className="mx-auto bg-color-success/10 p-3 rounded-full w-16 h-16 flex items-center justify-center mb-4">
<svg
className="w-8 h-8 text-color-success"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h1 className="text-2xl font-bold text-text-primary mb-2">
{t('auth:reset_password.success_title', '¡Contraseña Restablecida!')}
</h1>
<p className="text-text-secondary">
{t('auth:reset_password.success_message', 'Tu contraseña ha sido actualizada exitosamente.')}
</p>
</div>
<div className="space-y-4">
<Button
variant="primary"
size="lg"
isFullWidth
onClick={handleGoToLogin}
>
Iniciar Sesión
</Button>
</div>
</Card>
</PublicLayout>
);
}
return (
<PublicLayout
variant="centered"
maxWidth="md"
headerProps={{
showThemeToggle: true,
showAuthButtons: false,
showLanguageSelector: true,
variant: "minimal"
}}
>
<Card className="p-8 w-full max-w-md mx-auto" role="main">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-text-primary mb-2">
{t('auth:reset_password.title', 'Restablecer Contraseña')}
</h1>
<p className="text-text-secondary">
{t('auth:reset_password.subtitle', 'Ingresa tu nueva contraseña.')}
</p>
</div>
<form
onSubmit={handleSubmit}
className="space-y-6"
noValidate
aria-label={t('auth:reset_password.title', 'Formulario de restablecimiento de contraseña')}
>
<div>
<Input
type={showPassword ? 'text' : 'password'}
label="Nueva Contraseña"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={errors.password}
disabled={isSubmitting}
required
aria-describedby={errors.password ? 'password-error' : undefined}
leftIcon={
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
}
rightIcon={
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="text-text-secondary hover:text-text-primary transition-colors"
aria-label={showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
tabIndex={0}
>
{showPassword ? (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
</svg>
) : (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
}
/>
{errors.password && (
<p id="password-error" className="text-color-error text-sm mt-1" role="alert">
{errors.password}
</p>
)}
</div>
<div>
<Input
type={showConfirmPassword ? 'text' : 'password'}
label="Confirmar Nueva Contraseña"
placeholder="••••••••"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
error={errors.confirmPassword}
disabled={isSubmitting}
required
aria-describedby={errors.confirmPassword ? 'confirm-password-error' : undefined}
leftIcon={
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
}
rightIcon={
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="text-text-secondary hover:text-text-primary transition-colors"
aria-label={showConfirmPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
tabIndex={0}
>
{showConfirmPassword ? (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
</svg>
) : (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
}
/>
{errors.confirmPassword && (
<p id="confirm-password-error" className="text-color-error text-sm mt-1" role="alert">
{errors.confirmPassword}
</p>
)}
</div>
<Button
type="submit"
variant="primary"
size="lg"
isFullWidth
isLoading={isSubmitting}
loadingText="Actualizando contraseña..."
disabled={isSubmitting}
>
{isSubmitting ? 'Actualizando...' : 'Actualizar Contraseña'}
</Button>
</form>
<div className="mt-6 text-center">
<button
type="button"
onClick={handleGoToLogin}
className="text-sm text-color-primary hover:text-color-primary-dark transition-colors underline-offset-2 hover:underline focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-offset-2 rounded"
>
Volver al Inicio de Sesión
</button>
</div>
</Card>
</PublicLayout>
);
};
export default ResetPasswordPage;

View File

@@ -12,4 +12,6 @@ export { default as CareersPage } from './CareersPage';
export { default as HelpCenterPage } from './HelpCenterPage';
export { default as DocumentationPage } from './DocumentationPage';
export { default as ContactPage } from './ContactPage';
export { default as FeedbackPage } from './FeedbackPage';
export { default as FeedbackPage } from './FeedbackPage';
export { default as ForgotPasswordPage } from './ForgotPasswordPage';
export { default as ResetPasswordPage } from './ResetPasswordPage';