Improve the frontend 3
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Input, Card } from '../../ui';
|
||||
import { useAuthActions, useAuthLoading, useAuthError } from '../../../stores/auth.store';
|
||||
import { useToast } from '../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../utils/toast';
|
||||
|
||||
interface LoginFormProps {
|
||||
onSuccess?: () => void;
|
||||
@@ -38,7 +38,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||
const { login } = useAuthActions();
|
||||
const isLoading = useAuthLoading();
|
||||
const error = useAuthError();
|
||||
const { success, error: showError } = useToast();
|
||||
|
||||
|
||||
// Auto-focus on email field when component mounts
|
||||
useEffect(() => {
|
||||
@@ -78,7 +78,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||
|
||||
try {
|
||||
await login(credentials.email, credentials.password);
|
||||
success('¡Bienvenido de vuelta a tu panadería!', {
|
||||
showToast.success('¡Bienvenido de vuelta a tu panadería!', {
|
||||
title: 'Sesión iniciada correctamente'
|
||||
});
|
||||
onSuccess?.();
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Button, Input, Card } from '../../ui';
|
||||
import { PasswordCriteria, validatePassword, getPasswordErrors } from '../../ui/PasswordCriteria';
|
||||
import { useAuthActions } from '../../../stores/auth.store';
|
||||
import { useToast } from '../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../utils/toast';
|
||||
import { useResetPassword } from '../../../api/hooks/auth';
|
||||
|
||||
interface PasswordResetFormProps {
|
||||
@@ -39,7 +39,7 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
||||
const { mutateAsync: resetPasswordMutation, isPending: isResetting } = useResetPassword();
|
||||
const isLoading = isResetting;
|
||||
const error = null;
|
||||
const { showToast } = useToast();
|
||||
|
||||
|
||||
const isResetMode = Boolean(token) || mode === 'reset';
|
||||
|
||||
@@ -62,11 +62,9 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
||||
setIsTokenValid(isValidFormat);
|
||||
|
||||
if (!isValidFormat) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Token inválido',
|
||||
message: 'El enlace de restablecimiento no es válido o ha expirado'
|
||||
});
|
||||
showToast.error('El enlace de restablecimiento no es válido o ha expirado', {
|
||||
title: 'Token inválido'
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [token, showToast]);
|
||||
@@ -154,16 +152,12 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
||||
// Note: Password reset request functionality needs to be implemented in backend
|
||||
// For now, show a message that the feature is coming soon
|
||||
setIsEmailSent(true);
|
||||
showToast({
|
||||
type: 'info',
|
||||
title: 'Función en desarrollo',
|
||||
message: 'La solicitud de restablecimiento de contraseña estará disponible próximamente. Por favor, contacta al administrador.'
|
||||
showToast.info('La solicitud de restablecimiento de contraseña estará disponible próximamente. Por favor, contacta al administrador.', {
|
||||
title: 'Función en desarrollo'
|
||||
});
|
||||
} catch (err) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error de conexión',
|
||||
message: 'No se pudo conectar con el servidor. Verifica tu conexión a internet.'
|
||||
showToast.error('No se pudo conectar con el servidor. Verifica tu conexión a internet.', {
|
||||
title: 'Error de conexión'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -180,10 +174,8 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
||||
}
|
||||
|
||||
if (!token || isTokenValid === false) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Token inválido',
|
||||
message: 'El enlace de restablecimiento no es válido. Solicita uno nuevo.'
|
||||
showToast.error('El enlace de restablecimiento no es válido. Solicita uno nuevo.', {
|
||||
title: 'Token inválido'
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -195,18 +187,14 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
||||
new_password: password
|
||||
});
|
||||
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: 'Contraseña actualizada',
|
||||
message: '¡Tu contraseña ha sido restablecida exitosamente! Ya puedes iniciar sesión.'
|
||||
showToast.success('¡Tu contraseña ha sido restablecida exitosamente! Ya puedes iniciar sesión.', {
|
||||
title: 'Contraseña actualizada'
|
||||
});
|
||||
onSuccess?.();
|
||||
} catch (err: any) {
|
||||
const errorMessage = err?.response?.data?.detail || err?.message || 'El enlace ha expirado o no es válido. Solicita un nuevo restablecimiento.';
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error al restablecer contraseña',
|
||||
message: errorMessage
|
||||
showToast.error(errorMessage, {
|
||||
title: 'Error al restablecer contraseña'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -599,4 +587,4 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordResetForm;
|
||||
export default PasswordResetForm;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Button, Input, Card, Select, Avatar, Modal } from '../../ui';
|
||||
import { useAuthUser } from '../../../stores/auth.store';
|
||||
import { useToast } from '../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../utils/toast';
|
||||
import { useUpdateProfile, useChangePassword, useAuthProfile } from '../../../api/hooks/auth';
|
||||
|
||||
interface ProfileSettingsProps {
|
||||
@@ -42,7 +42,7 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
||||
initialTab = 'profile'
|
||||
}) => {
|
||||
const user = useAuthUser();
|
||||
const { showToast } = useToast();
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -139,20 +139,16 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Archivo inválido',
|
||||
message: 'Por favor, selecciona una imagen válida'
|
||||
showToast.error('Solo se permiten archivos de imagen (JPEG, PNG, GIF, WEBP)', {
|
||||
title: 'Error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Archivo muy grande',
|
||||
message: 'La imagen debe ser menor a 5MB'
|
||||
showToast.error('El archivo es demasiado grande. Máximo 5MB permitido', {
|
||||
title: 'Error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -174,16 +170,12 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
||||
setProfileData(prev => ({ ...prev, avatar: newImageUrl }));
|
||||
setHasChanges(prev => ({ ...prev, profile: true }));
|
||||
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: 'Imagen subida',
|
||||
message: 'Tu foto de perfil ha sido actualizada'
|
||||
showToast.success('¡Éxito!', {
|
||||
title: 'Foto de perfil actualizada correctamente'
|
||||
});
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error al subir imagen',
|
||||
message: 'No se pudo subir la imagen. Intenta de nuevo.'
|
||||
showToast.error('No se pudo actualizar la foto de perfil', {
|
||||
title: 'Error'
|
||||
});
|
||||
} finally {
|
||||
setUploadingImage(false);
|
||||
@@ -283,17 +275,13 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
||||
});
|
||||
|
||||
setHasChanges(false);
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: 'Perfil actualizado',
|
||||
message: 'Tu información ha sido guardada correctamente'
|
||||
showToast.success('¡Éxito!', {
|
||||
title: 'Perfil actualizado correctamente'
|
||||
});
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error al actualizar',
|
||||
message: 'No se pudo actualizar tu perfil'
|
||||
showToast.error('No se pudo actualizar el perfil', {
|
||||
title: 'Error'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -311,10 +299,8 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
||||
new_password: passwordData.newPassword,
|
||||
});
|
||||
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: 'Contraseña actualizada',
|
||||
message: 'Tu contraseña ha sido cambiada correctamente'
|
||||
showToast.success('¡Éxito!', {
|
||||
title: 'Contraseña cambiada correctamente'
|
||||
});
|
||||
|
||||
setPasswordData({
|
||||
@@ -323,10 +309,8 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
||||
confirmNewPassword: ''
|
||||
});
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error al cambiar contraseña',
|
||||
message: 'No se pudo cambiar tu contraseña. Por favor, verifica tu contraseña actual.'
|
||||
showToast.error('No se pudo cambiar la contraseña', {
|
||||
title: 'Error'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -725,4 +709,4 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileSettings;
|
||||
export default ProfileSettings;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import { Button, Input, Card } from '../../ui';
|
||||
import { PasswordCriteria, validatePassword, getPasswordErrors } from '../../ui/PasswordCriteria';
|
||||
import { useAuthActions, useAuthLoading, useAuthError } from '../../../stores/auth.store';
|
||||
import { useToast } from '../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../utils/toast';
|
||||
import { SubscriptionPricingCards } from '../../subscription/SubscriptionPricingCards';
|
||||
import PaymentForm from './PaymentForm';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
@@ -68,7 +68,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
const { register } = useAuthActions();
|
||||
const isLoading = useAuthLoading();
|
||||
const error = useAuthError();
|
||||
const { success: showSuccessToast, error: showErrorToast } = useToast();
|
||||
|
||||
|
||||
// Detect pilot program participation
|
||||
const { isPilot, couponCode, trialMonths } = usePilotDetection();
|
||||
@@ -236,12 +236,12 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
? '¡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), {
|
||||
showToast.success(t('auth:register.registering', successMessage), {
|
||||
title: t('auth:alerts.success_create', 'Cuenta creada exitosamente')
|
||||
});
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
showErrorToast(error || t('auth:register.register_button', 'No se pudo crear la cuenta. Verifica que el email no esté en uso.'), {
|
||||
showToast.error(error || t('auth:register.register_button', 'No se pudo crear la cuenta. Verifica que el email no esté en uso.'), {
|
||||
title: t('auth:alerts.error_create', 'Error al crear la cuenta')
|
||||
});
|
||||
}
|
||||
@@ -252,7 +252,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
};
|
||||
|
||||
const handlePaymentError = (errorMessage: string) => {
|
||||
showErrorToast(errorMessage, {
|
||||
showToast.error(errorMessage, {
|
||||
title: 'Error en el pago'
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Zap, Key, Settings as SettingsIcon, RefreshCw } from 'lucide-react';
|
||||
import { AddModal, AddModalSection, AddModalField } from '../../ui/AddModal/AddModal';
|
||||
import { posService } from '../../../api/services/pos';
|
||||
import { POSProviderConfig, POSSystem, POSEnvironment } from '../../../api/types/pos';
|
||||
import { useToast } from '../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../utils/toast';
|
||||
import { statusColors } from '../../../styles/colors';
|
||||
|
||||
interface CreatePOSConfigModalProps {
|
||||
@@ -29,7 +29,7 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedProvider, setSelectedProvider] = useState<POSSystem | ''>('');
|
||||
const { addToast } = useToast();
|
||||
|
||||
|
||||
// Initialize selectedProvider in edit mode
|
||||
React.useEffect(() => {
|
||||
@@ -250,7 +250,7 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
|
||||
// Find selected provider
|
||||
const provider = supportedProviders.find(p => p.id === formData.provider);
|
||||
if (!provider) {
|
||||
addToast('Por favor selecciona un sistema POS', { type: 'error' });
|
||||
showToast.error('Por favor selecciona un sistema POS');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -298,17 +298,17 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
|
||||
...payload,
|
||||
config_id: existingConfig.id
|
||||
});
|
||||
addToast('Configuración actualizada correctamente', { type: 'success' });
|
||||
showToast.success('Configuración actualizada correctamente');
|
||||
} else {
|
||||
await posService.createPOSConfiguration(payload);
|
||||
addToast('Configuración creada correctamente', { type: 'success' });
|
||||
showToast.success('Configuración creada correctamente');
|
||||
}
|
||||
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error('Error saving POS configuration:', error);
|
||||
addToast(error?.message || 'Error al guardar la configuración', { type: 'error' });
|
||||
showToast.error(error?.message || 'Error al guardar la configuración');
|
||||
throw error; // Let AddModal handle error state
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -345,7 +345,7 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
|
||||
// Custom validation if needed
|
||||
if (errors && Object.keys(errors).length > 0) {
|
||||
const firstError = Object.values(errors)[0];
|
||||
addToast(firstError, { type: 'error' });
|
||||
showToast.error(firstError);
|
||||
}
|
||||
}}
|
||||
onFieldChange={handleFieldChange}
|
||||
|
||||
@@ -396,6 +396,14 @@ export const Footer = forwardRef<FooterRef, FooterProps>(({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Made with love in Madrid */}
|
||||
{!compact && (
|
||||
<div className="flex items-center gap-2 text-[var(--text-tertiary)]">
|
||||
<Heart className="w-4 h-4 text-red-500 fill-red-500" />
|
||||
<span>{t('common:footer.made_with_love', 'Hecho con amor en Madrid')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Essential utilities only */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Privacy links - minimal set */}
|
||||
|
||||
@@ -168,12 +168,6 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
const baseNavigationRoutes = useMemo(() => getNavigationRoutes(), []);
|
||||
const { filteredRoutes: subscriptionFilteredRoutes } = useSubscriptionAwareRoutes(baseNavigationRoutes);
|
||||
|
||||
// Force re-render when subscription changes
|
||||
useEffect(() => {
|
||||
// The subscriptionVersion change will trigger a re-render
|
||||
// This ensures the sidebar picks up new route filtering based on updated subscription
|
||||
}, [subscriptionVersion]);
|
||||
|
||||
// Map route paths to translation keys
|
||||
const getTranslationKey = (routePath: string): string => {
|
||||
const pathMappings: Record<string, string> = {
|
||||
|
||||
46
frontend/src/components/ui/Slider/Slider.tsx
Normal file
46
frontend/src/components/ui/Slider/Slider.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface SliderProps {
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
value: number[];
|
||||
onValueChange: (value: number[]) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Slider: React.FC<SliderProps> = ({
|
||||
min,
|
||||
max,
|
||||
step = 1,
|
||||
value,
|
||||
onValueChange,
|
||||
disabled = false,
|
||||
className = '',
|
||||
}) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = parseFloat(e.target.value);
|
||||
onValueChange([newValue]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex items-center space-x-4 ${className}`}>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value[0]}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
className="w-full h-2 bg-[var(--bg-secondary)] rounded-lg appearance-none cursor-pointer accent-[var(--color-primary)] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<span className="text-sm text-[var(--text-secondary)] min-w-12">
|
||||
{(value[0] * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Slider;
|
||||
3
frontend/src/components/ui/Slider/index.ts
Normal file
3
frontend/src/components/ui/Slider/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default } from './Slider';
|
||||
export { default as Slider } from './Slider';
|
||||
export type { SliderProps } from './Slider';
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTenant } from '../../stores/tenant.store';
|
||||
import { useToast } from '../../hooks/ui/useToast';
|
||||
import { showToast } from '../../utils/toast';
|
||||
import { ChevronDown, Building2, Check, AlertCircle, Plus, X } from 'lucide-react';
|
||||
|
||||
interface TenantSwitcherProps {
|
||||
@@ -36,7 +36,7 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
clearError,
|
||||
} = useTenant();
|
||||
|
||||
const { success: showSuccessToast, error: showErrorToast } = useToast();
|
||||
|
||||
|
||||
// Load tenants on mount
|
||||
useEffect(() => {
|
||||
@@ -150,11 +150,11 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
|
||||
if (success) {
|
||||
const newTenant = availableTenants?.find(t => t.id === tenantId);
|
||||
showSuccessToast(`Switched to ${newTenant?.name}`, {
|
||||
showToast.success(`Switched to ${newTenant?.name}`, {
|
||||
title: 'Tenant Switched'
|
||||
});
|
||||
} else {
|
||||
showErrorToast(error || 'Failed to switch tenant', {
|
||||
showToast.error(error || 'Failed to switch tenant', {
|
||||
title: 'Switch Failed'
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user