Initial commit from remix
This commit is contained in:
332
src/contexts/AuthContext.tsx
Normal file
332
src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,332 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { apiClient } from '@/services/adminApi';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
type: 'tourist' | 'business';
|
||||
role?: 'tourist' | 'taxi' | 'guide' | 'restaurant' | 'hotel' | 'politur' | 'admin' | 'super_admin';
|
||||
avatar: string;
|
||||
location: { lat: number; lng: number };
|
||||
preferences: { language: string };
|
||||
wallet?: { balance: number; currency: string };
|
||||
profile?: {
|
||||
phone: string;
|
||||
address: string;
|
||||
joinedDate: string;
|
||||
permissions: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
isLoading: boolean;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
register: (userData: any) => Promise<void>;
|
||||
logout: () => void;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
const storedToken = localStorage.getItem('karibeo-token');
|
||||
const storedUser = localStorage.getItem('karibeo-user');
|
||||
if (storedUser) {
|
||||
try {
|
||||
const parsed = JSON.parse(storedUser);
|
||||
const roleNum = parsed?.role_id ?? parsed?.roleId ?? parsed?.role?.id;
|
||||
const roleStr = typeof parsed?.role === 'string' ? parsed.role : undefined;
|
||||
const ROLE_MAP: Record<number, string> = { 1: 'super_admin', 2: 'admin', 3: 'taxi', 4: 'tourist', 5: 'guide', 6: 'hotel' };
|
||||
const normalizedRole = (roleStr?.toLowerCase?.()) || (roleNum ? ROLE_MAP[Number(roleNum)] : undefined);
|
||||
const normalizedUser = normalizedRole ? { ...parsed, role: normalizedRole } : parsed;
|
||||
setUser(normalizedUser);
|
||||
if (normalizedRole && normalizedRole !== parsed?.role) {
|
||||
localStorage.setItem('karibeo-user', JSON.stringify(normalizedUser));
|
||||
}
|
||||
} catch {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
} else if (storedToken) {
|
||||
// Avoid calling /auth/profile which returns 401 in this environment
|
||||
// We'll rely on stored user until profile endpoint is accessible
|
||||
console.warn('Token found but no cached user; skipping /auth/profile on init');
|
||||
}
|
||||
} catch (e) {
|
||||
// Don't aggressively clear token; just reset user
|
||||
setUser(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log('Login attempt with:', { email, password: '***' });
|
||||
|
||||
// Mock users for testing
|
||||
const mockUsers = {
|
||||
'superadmin@karibeo.com': {
|
||||
id: '1',
|
||||
email: 'superadmin@karibeo.com',
|
||||
name: 'Super Admin',
|
||||
role: 'super_admin' as const,
|
||||
type: 'business' as const,
|
||||
avatar: '/api/placeholder/40/40',
|
||||
location: { lat: 18.4861, lng: -69.9312 },
|
||||
preferences: { language: 'es' },
|
||||
wallet: { balance: 0, currency: 'USD' },
|
||||
profile: {
|
||||
phone: '+1-809-555-0001',
|
||||
address: 'Santo Domingo, República Dominicana',
|
||||
joinedDate: '2023-01-01',
|
||||
permissions: ['all']
|
||||
}
|
||||
},
|
||||
'admin@karibeo.com': {
|
||||
id: '2',
|
||||
email: 'admin@karibeo.com',
|
||||
name: 'Admin User',
|
||||
role: 'admin' as const,
|
||||
type: 'business' as const,
|
||||
avatar: '/api/placeholder/40/40',
|
||||
location: { lat: 18.4861, lng: -69.9312 },
|
||||
preferences: { language: 'es' },
|
||||
wallet: { balance: 0, currency: 'USD' },
|
||||
profile: {
|
||||
phone: '+1-809-555-0002',
|
||||
address: 'Santiago, República Dominicana',
|
||||
joinedDate: '2023-02-01',
|
||||
permissions: ['user_management', 'content_management']
|
||||
}
|
||||
},
|
||||
'user@karibeo.com': {
|
||||
id: '3',
|
||||
email: 'user@karibeo.com',
|
||||
name: 'Regular User',
|
||||
role: 'tourist' as const,
|
||||
type: 'tourist' as const,
|
||||
avatar: '/api/placeholder/40/40',
|
||||
location: { lat: 18.4861, lng: -69.9312 },
|
||||
preferences: { language: 'es' },
|
||||
wallet: { balance: 150.50, currency: 'USD' },
|
||||
profile: {
|
||||
phone: '+1-809-555-0003',
|
||||
address: 'Punta Cana, República Dominicana',
|
||||
joinedDate: '2023-03-01',
|
||||
permissions: ['booking', 'reviews']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check if it's a mock user
|
||||
const mockUser = mockUsers[email as keyof typeof mockUsers];
|
||||
if (mockUser && password === '123456') {
|
||||
console.log('🎯 Mock login successful for:', email, 'with role:', mockUser.role);
|
||||
const token = `mock-token-${Date.now()}`;
|
||||
localStorage.setItem('karibeo-token', token);
|
||||
localStorage.setItem('karibeo_token', token);
|
||||
localStorage.setItem('karibeo-user', JSON.stringify(mockUser));
|
||||
setUser(mockUser);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const loginData = { email: email.trim(), password: password.trim() };
|
||||
console.log('Sending login data (form first):', { email: loginData.email, password: '***' });
|
||||
|
||||
let loginRes: any;
|
||||
// Try application/x-www-form-urlencoded first (some backends validate this path)
|
||||
try {
|
||||
loginRes = await apiClient.postForm('/auth/login', {
|
||||
email: loginData.email,
|
||||
password: loginData.password,
|
||||
});
|
||||
} catch (firstErr: any) {
|
||||
console.warn('Form login failed, retrying as JSON:', firstErr?.message);
|
||||
loginRes = await apiClient.post('/auth/login', loginData);
|
||||
}
|
||||
|
||||
console.log('Login response:', loginRes);
|
||||
const token = loginRes?.token || loginRes?.accessToken || loginRes?.access_token;
|
||||
const refresh = loginRes?.refreshToken || loginRes?.refresh_token;
|
||||
if (!token) {
|
||||
throw new Error('Credenciales inválidas');
|
||||
}
|
||||
localStorage.setItem('karibeo-token', token);
|
||||
localStorage.setItem('karibeo_token', token);
|
||||
if (refresh) {
|
||||
localStorage.setItem('karibeo-refresh', refresh);
|
||||
localStorage.setItem('karibeo_refresh', refresh);
|
||||
}
|
||||
|
||||
let userData = (loginRes && (loginRes.user || loginRes.profile || null)) as any;
|
||||
|
||||
// Try to enrich with /auth/profile (post-login, should be authorized)
|
||||
try {
|
||||
const profile = await apiClient.get('/auth/profile') as any;
|
||||
const profileUser = (profile && (profile.user || profile)) as any;
|
||||
if (profileUser) {
|
||||
userData = { ...(userData || {}), ...profileUser };
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn('Skipping /auth/profile after login:', e?.message);
|
||||
}
|
||||
|
||||
if (userData) {
|
||||
// Enhanced role detection and normalization
|
||||
let normalizedRole = '';
|
||||
|
||||
// Try role.name first (most reliable)
|
||||
if (userData?.role?.name) {
|
||||
const roleRaw = userData.role.name.toString().trim();
|
||||
const roleName = roleRaw.toLowerCase();
|
||||
console.log('🔍 Raw role.name from API:', userData.role.name, '-> normalized?', roleName);
|
||||
if (roleName.includes('super') && roleName.includes('admin')) {
|
||||
normalizedRole = 'super_admin';
|
||||
} else if (roleName.includes('admin')) {
|
||||
normalizedRole = 'admin';
|
||||
} else if (roleName.includes('taxi')) {
|
||||
normalizedRole = 'taxi';
|
||||
} else if (roleName.includes('guide')) {
|
||||
normalizedRole = 'guide';
|
||||
} else if (roleName.includes('hotel')) {
|
||||
normalizedRole = 'hotel';
|
||||
} else if (roleName.includes('tourist')) {
|
||||
normalizedRole = 'tourist';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to roleId mapping
|
||||
if (!normalizedRole && userData?.roleId) {
|
||||
const ROLE_MAP: Record<number, string> = { 1: 'super_admin', 2: 'admin', 3: 'taxi', 4: 'tourist', 5: 'guide', 6: 'hotel', 7: 'restaurant', 8: 'business', 9: 'staff' };
|
||||
normalizedRole = ROLE_MAP[Number(userData.roleId)] || '';
|
||||
console.log('🔍 Using roleId fallback:', userData.roleId, '-> mapped to:', normalizedRole);
|
||||
}
|
||||
|
||||
// Fallback to JWT token role
|
||||
if (!normalizedRole) {
|
||||
try {
|
||||
const payload = JSON.parse(atob(String(token).split('.')[1]));
|
||||
const tokenRole = payload?.role;
|
||||
if (tokenRole) {
|
||||
const tokenRoleNorm = tokenRole.toString().trim().toLowerCase();
|
||||
console.log('🔍 JWT token role:', tokenRole, '-> normalized:', tokenRoleNorm);
|
||||
if (tokenRoleNorm.includes('super') && tokenRoleNorm.includes('admin')) {
|
||||
normalizedRole = 'super_admin';
|
||||
} else if (tokenRoleNorm.includes('admin')) {
|
||||
normalizedRole = 'admin';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse JWT token:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const finalUser = { ...userData, role: normalizedRole };
|
||||
console.log('🎯 Final user with normalized role:', { email: finalUser.email, role: finalUser.role, roleId: finalUser.roleId });
|
||||
|
||||
setUser(finalUser);
|
||||
localStorage.setItem('karibeo-user', JSON.stringify(finalUser));
|
||||
} else {
|
||||
// If backend doesn't return user, skip extra call and set minimal user from token
|
||||
setUser({
|
||||
id: '',
|
||||
email,
|
||||
name: email.split('@')[0],
|
||||
type: 'tourist',
|
||||
avatar: '',
|
||||
location: { lat: 0, lng: 0 },
|
||||
preferences: { language: 'es' },
|
||||
} as any);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Login error:', error);
|
||||
localStorage.removeItem('karibeo-token');
|
||||
localStorage.removeItem('karibeo-user');
|
||||
setUser(null);
|
||||
throw new Error(error?.message || 'No se pudo iniciar sesión');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (userData: any) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await apiClient.post('/auth/register', userData) as any;
|
||||
const token = res?.token || res?.accessToken || res?.access_token;
|
||||
const refresh = res?.refreshToken || res?.refresh_token;
|
||||
if (token) {
|
||||
localStorage.setItem('karibeo-token', token);
|
||||
localStorage.setItem('karibeo_token', token);
|
||||
}
|
||||
if (refresh) {
|
||||
localStorage.setItem('karibeo-refresh', refresh);
|
||||
localStorage.setItem('karibeo_refresh', refresh);
|
||||
}
|
||||
|
||||
const profile = await apiClient.get('/auth/profile') as any;
|
||||
const newUser = (profile && (profile.user || profile)) as any;
|
||||
|
||||
setUser(newUser);
|
||||
localStorage.setItem('karibeo-user', JSON.stringify(newUser));
|
||||
} catch (error: any) {
|
||||
localStorage.removeItem('karibeo-token');
|
||||
localStorage.removeItem('karibeo-user');
|
||||
setUser(null);
|
||||
throw new Error(error?.message || 'No se pudo registrar');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
localStorage.removeItem('karibeo-user');
|
||||
localStorage.removeItem('karibeo-token');
|
||||
localStorage.removeItem('karibeo_token');
|
||||
localStorage.removeItem('karibeo-refresh');
|
||||
localStorage.removeItem('karibeo_refresh');
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{
|
||||
user,
|
||||
isLoading,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
isAuthenticated: !!user
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
// Soft fallback to avoid app crash; helps while provider is being hot-reloaded
|
||||
console.warn('useAuth used outside AuthProvider. Using safe fallback.');
|
||||
return {
|
||||
user: null,
|
||||
isLoading: false,
|
||||
login: async () => { throw new Error('AuthProvider no inicializado'); },
|
||||
register: async () => { throw new Error('AuthProvider no inicializado'); },
|
||||
logout: () => {},
|
||||
isAuthenticated: false,
|
||||
} as AuthContextType;
|
||||
}
|
||||
return context;
|
||||
};
|
||||
111
src/contexts/CartContext.tsx
Normal file
111
src/contexts/CartContext.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
interface CartItem {
|
||||
id: string;
|
||||
title: string;
|
||||
price: number;
|
||||
image: string;
|
||||
category: string;
|
||||
location: string;
|
||||
quantity: number;
|
||||
selectedDate?: string;
|
||||
guests?: number;
|
||||
}
|
||||
|
||||
interface CartContextType {
|
||||
items: CartItem[];
|
||||
addToCart: (item: Omit<CartItem, 'quantity'>, quantity?: number) => void;
|
||||
removeFromCart: (id: string) => void;
|
||||
updateQuantity: (id: string, quantity: number) => void;
|
||||
clearCart: () => void;
|
||||
getTotalItems: () => number;
|
||||
getTotalPrice: () => number;
|
||||
}
|
||||
|
||||
const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||
|
||||
export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [items, setItems] = useState<CartItem[]>([]);
|
||||
|
||||
// Load cart from localStorage on component mount
|
||||
useEffect(() => {
|
||||
const savedCart = localStorage.getItem('karibeo_cart');
|
||||
if (savedCart) {
|
||||
try {
|
||||
setItems(JSON.parse(savedCart));
|
||||
} catch (error) {
|
||||
console.error('Error loading cart from localStorage:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Save cart to localStorage whenever items change
|
||||
useEffect(() => {
|
||||
localStorage.setItem('karibeo_cart', JSON.stringify(items));
|
||||
}, [items]);
|
||||
|
||||
const addToCart = (item: Omit<CartItem, 'quantity'>, quantity = 1) => {
|
||||
setItems(prevItems => {
|
||||
const existingItem = prevItems.find(cartItem => cartItem.id === item.id);
|
||||
if (existingItem) {
|
||||
return prevItems.map(cartItem =>
|
||||
cartItem.id === item.id
|
||||
? { ...cartItem, quantity: cartItem.quantity + quantity }
|
||||
: cartItem
|
||||
);
|
||||
}
|
||||
return [...prevItems, { ...item, quantity }];
|
||||
});
|
||||
};
|
||||
|
||||
const removeFromCart = (id: string) => {
|
||||
setItems(prevItems => prevItems.filter(item => item.id !== id));
|
||||
};
|
||||
|
||||
const updateQuantity = (id: string, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
removeFromCart(id);
|
||||
return;
|
||||
}
|
||||
setItems(prevItems =>
|
||||
prevItems.map(item =>
|
||||
item.id === id ? { ...item, quantity } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const clearCart = () => {
|
||||
setItems([]);
|
||||
localStorage.removeItem('karibeo_cart');
|
||||
};
|
||||
|
||||
const getTotalItems = () => {
|
||||
return items.reduce((total, item) => total + item.quantity, 0);
|
||||
};
|
||||
|
||||
const getTotalPrice = () => {
|
||||
return items.reduce((total, item) => total + (item.price * item.quantity), 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<CartContext.Provider value={{
|
||||
items,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
updateQuantity,
|
||||
clearCart,
|
||||
getTotalItems,
|
||||
getTotalPrice
|
||||
}}>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCart = () => {
|
||||
const context = useContext(CartContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useCart must be used within a CartProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
48
src/contexts/LanguageContext.tsx
Normal file
48
src/contexts/LanguageContext.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { translations } from '@/i18n/translations';
|
||||
|
||||
type Language = 'es' | 'en' | 'fr';
|
||||
type TranslationKey = keyof typeof translations.es;
|
||||
|
||||
interface LanguageContextType {
|
||||
language: Language;
|
||||
setLanguage: (lang: Language) => void;
|
||||
t: (key: TranslationKey) => string;
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [language, setLanguageState] = useState<Language>(() => {
|
||||
const saved = localStorage.getItem('karibeo-language');
|
||||
return (saved as Language) || 'es';
|
||||
});
|
||||
|
||||
const setLanguage = (lang: Language) => {
|
||||
setLanguageState(lang);
|
||||
localStorage.setItem('karibeo-language', lang);
|
||||
};
|
||||
|
||||
const t = (key: TranslationKey): string => {
|
||||
return translations[language][key] || translations.es[key] || key;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Update document language
|
||||
document.documentElement.lang = language;
|
||||
}, [language]);
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ language, setLanguage, t }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useLanguage = () => {
|
||||
const context = useContext(LanguageContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user