mas cambios
This commit is contained in:
224
src/hooks/useCollections.ts
Normal file
224
src/hooks/useCollections.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { collectionsApi, Collection, CollectionStats, CreateCollectionDto, UpdateCollectionDto, AddCollectionItemDto } from '@/services/collectionsApi';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useCollections = () => {
|
||||
const { user } = useAuth();
|
||||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [stats, setStats] = useState<CollectionStats | null>(null);
|
||||
const [selectedCollection, setSelectedCollection] = useState<Collection | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Cargar colecciones
|
||||
const loadCollections = useCallback(async () => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await collectionsApi.getMyCollections();
|
||||
setCollections(data);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Error al cargar colecciones';
|
||||
setError(errorMessage);
|
||||
console.error('Error loading collections:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
// Cargar estadísticas
|
||||
const loadStats = useCallback(async () => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
const data = await collectionsApi.getCollectionsStats();
|
||||
setStats(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading collections stats:', err);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
// Crear colección
|
||||
const createCollection = useCallback(async (data: CreateCollectionDto): Promise<Collection | null> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para crear colecciones');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const newCollection = await collectionsApi.createCollection(data);
|
||||
toast.success('Colección creada');
|
||||
await loadCollections();
|
||||
await loadStats();
|
||||
return newCollection;
|
||||
} catch (err) {
|
||||
console.error('Error creating collection:', err);
|
||||
toast.error('Error al crear colección');
|
||||
return null;
|
||||
}
|
||||
}, [user?.id, loadCollections, loadStats]);
|
||||
|
||||
// Obtener colección por ID
|
||||
const getCollectionById = useCallback(async (id: string): Promise<Collection | null> => {
|
||||
try {
|
||||
const collection = await collectionsApi.getCollectionById(id);
|
||||
setSelectedCollection(collection);
|
||||
return collection;
|
||||
} catch (err) {
|
||||
console.error('Error fetching collection:', err);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Actualizar colección
|
||||
const updateCollection = useCallback(async (id: string, data: UpdateCollectionDto): Promise<boolean> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para actualizar colecciones');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await collectionsApi.updateCollection(id, data);
|
||||
toast.success('Colección actualizada');
|
||||
await loadCollections();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error updating collection:', err);
|
||||
toast.error('Error al actualizar colección');
|
||||
return false;
|
||||
}
|
||||
}, [user?.id, loadCollections]);
|
||||
|
||||
// Eliminar colección
|
||||
const deleteCollection = useCallback(async (id: string): Promise<boolean> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para eliminar colecciones');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await collectionsApi.deleteCollection(id);
|
||||
toast.success('Colección eliminada');
|
||||
setCollections(prev => prev.filter(c => c.id !== id));
|
||||
await loadStats();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error deleting collection:', err);
|
||||
toast.error('Error al eliminar colección');
|
||||
return false;
|
||||
}
|
||||
}, [user?.id, loadStats]);
|
||||
|
||||
// Agregar item a colección
|
||||
const addItemToCollection = useCallback(async (collectionId: string, data: AddCollectionItemDto): Promise<boolean> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para agregar items');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await collectionsApi.addItemToCollection(collectionId, data);
|
||||
toast.success('Item agregado a la colección');
|
||||
// Recargar la colección si está seleccionada
|
||||
if (selectedCollection?.id === collectionId) {
|
||||
await getCollectionById(collectionId);
|
||||
}
|
||||
await loadCollections();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error adding item to collection:', err);
|
||||
toast.error('Error al agregar item');
|
||||
return false;
|
||||
}
|
||||
}, [user?.id, selectedCollection?.id, getCollectionById, loadCollections]);
|
||||
|
||||
// Quitar item de colección
|
||||
const removeItemFromCollection = useCallback(async (collectionId: string, itemId: string): Promise<boolean> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para quitar items');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await collectionsApi.removeItemFromCollection(collectionId, itemId);
|
||||
toast.success('Item eliminado de la colección');
|
||||
// Actualizar colección seleccionada
|
||||
if (selectedCollection?.id === collectionId) {
|
||||
await getCollectionById(collectionId);
|
||||
}
|
||||
await loadCollections();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error removing item from collection:', err);
|
||||
toast.error('Error al eliminar item');
|
||||
return false;
|
||||
}
|
||||
}, [user?.id, selectedCollection?.id, getCollectionById, loadCollections]);
|
||||
|
||||
// Reordenar items
|
||||
const reorderItems = useCallback(async (collectionId: string, itemIds: string[]): Promise<boolean> => {
|
||||
try {
|
||||
await collectionsApi.reorderCollectionItems(collectionId, itemIds);
|
||||
if (selectedCollection?.id === collectionId) {
|
||||
await getCollectionById(collectionId);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error reordering items:', err);
|
||||
toast.error('Error al reordenar items');
|
||||
return false;
|
||||
}
|
||||
}, [selectedCollection?.id, getCollectionById]);
|
||||
|
||||
// Reordenar colecciones
|
||||
const reorderCollections = useCallback(async (collectionIds: string[]): Promise<boolean> => {
|
||||
try {
|
||||
await collectionsApi.reorderCollections(collectionIds);
|
||||
await loadCollections();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error reordering collections:', err);
|
||||
toast.error('Error al reordenar colecciones');
|
||||
return false;
|
||||
}
|
||||
}, [loadCollections]);
|
||||
|
||||
// Limpiar error
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Carga inicial
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
loadCollections();
|
||||
loadStats();
|
||||
}
|
||||
}, [user?.id, loadCollections, loadStats]);
|
||||
|
||||
return {
|
||||
collections,
|
||||
stats,
|
||||
selectedCollection,
|
||||
loading,
|
||||
error,
|
||||
loadCollections,
|
||||
loadStats,
|
||||
createCollection,
|
||||
getCollectionById,
|
||||
updateCollection,
|
||||
deleteCollection,
|
||||
addItemToCollection,
|
||||
removeItemFromCollection,
|
||||
reorderItems,
|
||||
reorderCollections,
|
||||
setSelectedCollection,
|
||||
clearError,
|
||||
getCollectionsCount: () => collections.length,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCollections;
|
||||
176
src/hooks/useFavorites.ts
Normal file
176
src/hooks/useFavorites.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { favoritesApi, Favorite, FavoriteItemType, CreateFavoriteDto, FavoritesCounts } from '@/services/favoritesApi';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useFavorites = (initialItemType?: FavoriteItemType) => {
|
||||
const { user } = useAuth();
|
||||
const [favorites, setFavorites] = useState<Favorite[]>([]);
|
||||
const [counts, setCounts] = useState<FavoritesCounts | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedType, setSelectedType] = useState<FavoriteItemType | undefined>(initialItemType);
|
||||
|
||||
// Cargar favoritos
|
||||
const loadFavorites = useCallback(async (itemType?: FavoriteItemType) => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await favoritesApi.getMyFavorites(itemType || selectedType);
|
||||
setFavorites(data);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Error al cargar favoritos';
|
||||
setError(errorMessage);
|
||||
console.error('Error loading favorites:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user?.id, selectedType]);
|
||||
|
||||
// Cargar conteos
|
||||
const loadCounts = useCallback(async () => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
const data = await favoritesApi.getFavoritesCounts();
|
||||
setCounts(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading favorites counts:', err);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
// Agregar a favoritos
|
||||
const addFavorite = useCallback(async (data: CreateFavoriteDto): Promise<boolean> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para agregar favoritos');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await favoritesApi.addFavorite(data);
|
||||
toast.success('Agregado a favoritos');
|
||||
await loadFavorites();
|
||||
await loadCounts();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error adding favorite:', err);
|
||||
toast.error('Error al agregar favorito');
|
||||
return false;
|
||||
}
|
||||
}, [user?.id, loadFavorites, loadCounts]);
|
||||
|
||||
// Toggle favorito
|
||||
const toggleFavorite = useCallback(async (data: CreateFavoriteDto): Promise<boolean> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para gestionar favoritos');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await favoritesApi.toggleFavorite(data);
|
||||
if (result.action === 'added') {
|
||||
toast.success('Agregado a favoritos');
|
||||
} else {
|
||||
toast.success('Eliminado de favoritos');
|
||||
}
|
||||
await loadFavorites();
|
||||
await loadCounts();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error toggling favorite:', err);
|
||||
toast.error('Error al actualizar favorito');
|
||||
return false;
|
||||
}
|
||||
}, [user?.id, loadFavorites, loadCounts]);
|
||||
|
||||
// Eliminar favorito
|
||||
const removeFavorite = useCallback(async (favoriteId: string): Promise<boolean> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para gestionar favoritos');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await favoritesApi.removeFavorite(favoriteId);
|
||||
toast.success('Eliminado de favoritos');
|
||||
setFavorites(prev => prev.filter(f => f.id !== favoriteId));
|
||||
await loadCounts();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error removing favorite:', err);
|
||||
toast.error('Error al eliminar favorito');
|
||||
return false;
|
||||
}
|
||||
}, [user?.id, loadCounts]);
|
||||
|
||||
// Verificar si es favorito
|
||||
const checkFavorite = useCallback(async (itemType: FavoriteItemType, itemId: string): Promise<boolean> => {
|
||||
if (!user?.id) return false;
|
||||
|
||||
try {
|
||||
const result = await favoritesApi.checkFavorite(itemType, itemId);
|
||||
return result.isFavorite;
|
||||
} catch (err) {
|
||||
console.error('Error checking favorite:', err);
|
||||
return false;
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
// Verificar si un item está en los favoritos cargados
|
||||
const isFavorite = useCallback((itemId: string, itemType?: FavoriteItemType): boolean => {
|
||||
return favorites.some(f => f.itemId === itemId && (!itemType || f.itemType === itemType));
|
||||
}, [favorites]);
|
||||
|
||||
// Obtener favorito por ID
|
||||
const getFavoriteById = useCallback((favoriteId: string): Favorite | undefined => {
|
||||
return favorites.find(f => f.id === favoriteId);
|
||||
}, [favorites]);
|
||||
|
||||
// Filtrar por tipo
|
||||
const filterByType = useCallback((type: FavoriteItemType): Favorite[] => {
|
||||
return favorites.filter(f => f.itemType === type);
|
||||
}, [favorites]);
|
||||
|
||||
// Cambiar tipo seleccionado
|
||||
const changeType = useCallback((type?: FavoriteItemType) => {
|
||||
setSelectedType(type);
|
||||
loadFavorites(type);
|
||||
}, [loadFavorites]);
|
||||
|
||||
// Limpiar error
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Carga inicial
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
loadFavorites();
|
||||
loadCounts();
|
||||
}
|
||||
}, [user?.id, loadFavorites, loadCounts]);
|
||||
|
||||
return {
|
||||
favorites,
|
||||
counts,
|
||||
loading,
|
||||
error,
|
||||
selectedType,
|
||||
loadFavorites,
|
||||
loadCounts,
|
||||
addFavorite,
|
||||
toggleFavorite,
|
||||
removeFavorite,
|
||||
checkFavorite,
|
||||
isFavorite,
|
||||
getFavoriteById,
|
||||
filterByType,
|
||||
changeType,
|
||||
clearError,
|
||||
getFavoritesCount: () => favorites.length,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFavorites;
|
||||
177
src/hooks/useNotifications.ts
Normal file
177
src/hooks/useNotifications.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* useNotifications Hook
|
||||
* Hook para gestionar notificaciones en el Dashboard - Fase 2
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { notificationsApi, Notification, NotificationType, NotificationStats } from '@/services/notificationsApi';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
|
||||
interface UseNotificationsOptions {
|
||||
autoFetch?: boolean;
|
||||
pollingInterval?: number; // en milisegundos, 0 = sin polling
|
||||
}
|
||||
|
||||
interface UseNotificationsReturn {
|
||||
notifications: Notification[];
|
||||
unreadCount: number;
|
||||
stats: NotificationStats | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
// Actions
|
||||
fetchNotifications: (type?: NotificationType, isRead?: boolean) => Promise<void>;
|
||||
markAsRead: (notificationId: string) => Promise<void>;
|
||||
markAllAsRead: () => Promise<void>;
|
||||
deleteNotification: (notificationId: string) => Promise<void>;
|
||||
deleteAllRead: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useNotifications(options: UseNotificationsOptions = {}): UseNotificationsReturn {
|
||||
const { autoFetch = true, pollingInterval = 0 } = options;
|
||||
const { toast } = useToast();
|
||||
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [stats, setStats] = useState<NotificationStats | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchNotifications = useCallback(async (type?: NotificationType, isRead?: boolean) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await notificationsApi.getMyNotifications({ type, isRead, limit: 50 });
|
||||
setNotifications(result.notifications);
|
||||
setUnreadCount(result.unreadCount);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Error al cargar notificaciones';
|
||||
setError(message);
|
||||
console.error('Error fetching notifications:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchUnreadCount = useCallback(async () => {
|
||||
try {
|
||||
const count = await notificationsApi.getUnreadCount();
|
||||
setUnreadCount(count);
|
||||
} catch (err) {
|
||||
console.error('Error fetching unread count:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const markAsRead = useCallback(async (notificationId: string) => {
|
||||
try {
|
||||
await notificationsApi.markAsRead(notificationId);
|
||||
setNotifications(prev =>
|
||||
prev.map(n => n.id === notificationId ? { ...n, isRead: true, readAt: new Date().toISOString() } : n)
|
||||
);
|
||||
setUnreadCount(prev => Math.max(0, prev - 1));
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'No se pudo marcar la notificacion como leida',
|
||||
variant: 'destructive',
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}, [toast]);
|
||||
|
||||
const markAllAsRead = useCallback(async () => {
|
||||
try {
|
||||
const result = await notificationsApi.markAllAsRead();
|
||||
setNotifications(prev =>
|
||||
prev.map(n => ({ ...n, isRead: true, readAt: new Date().toISOString() }))
|
||||
);
|
||||
setUnreadCount(0);
|
||||
toast({
|
||||
title: 'Listo',
|
||||
description: `${result.count} notificaciones marcadas como leidas`,
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'No se pudieron marcar las notificaciones como leidas',
|
||||
variant: 'destructive',
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}, [toast]);
|
||||
|
||||
const deleteNotification = useCallback(async (notificationId: string) => {
|
||||
try {
|
||||
const notification = notifications.find(n => n.id === notificationId);
|
||||
await notificationsApi.deleteNotification(notificationId);
|
||||
setNotifications(prev => prev.filter(n => n.id !== notificationId));
|
||||
if (notification && !notification.isRead) {
|
||||
setUnreadCount(prev => Math.max(0, prev - 1));
|
||||
}
|
||||
toast({
|
||||
title: 'Eliminada',
|
||||
description: 'Notificacion eliminada correctamente',
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'No se pudo eliminar la notificacion',
|
||||
variant: 'destructive',
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}, [notifications, toast]);
|
||||
|
||||
const deleteAllRead = useCallback(async () => {
|
||||
try {
|
||||
const result = await notificationsApi.deleteAllRead();
|
||||
setNotifications(prev => prev.filter(n => !n.isRead));
|
||||
toast({
|
||||
title: 'Listo',
|
||||
description: `${result.count} notificaciones eliminadas`,
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'No se pudieron eliminar las notificaciones',
|
||||
variant: 'destructive',
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}, [toast]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchNotifications();
|
||||
}, [fetchNotifications]);
|
||||
|
||||
// Auto fetch on mount
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchNotifications();
|
||||
}
|
||||
}, [autoFetch, fetchNotifications]);
|
||||
|
||||
// Polling for unread count
|
||||
useEffect(() => {
|
||||
if (pollingInterval > 0) {
|
||||
const interval = setInterval(fetchUnreadCount, pollingInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [pollingInterval, fetchUnreadCount]);
|
||||
|
||||
return {
|
||||
notifications,
|
||||
unreadCount,
|
||||
stats,
|
||||
loading,
|
||||
error,
|
||||
fetchNotifications,
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
deleteNotification,
|
||||
deleteAllRead,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
|
||||
export default useNotifications;
|
||||
211
src/hooks/useQuiz.ts
Normal file
211
src/hooks/useQuiz.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { quizApi, QuizQuestion, QuizResponse, SubmitQuizDto, QuizAnswer } from '@/services/quizApi';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useQuiz = () => {
|
||||
const { user } = useAuth();
|
||||
const [questions, setQuestions] = useState<QuizQuestion[]>([]);
|
||||
const [quizResponse, setQuizResponse] = useState<QuizResponse | null>(null);
|
||||
const [currentAnswers, setCurrentAnswers] = useState<QuizAnswer[]>([]);
|
||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Cargar preguntas
|
||||
const loadQuestions = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await quizApi.getQuestions();
|
||||
setQuestions(data);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Error al cargar preguntas';
|
||||
setError(errorMessage);
|
||||
console.error('Error loading questions:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Cargar respuesta del usuario
|
||||
const loadMyResponse = useCallback(async () => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
const data = await quizApi.getMyQuizResponse();
|
||||
setQuizResponse(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading quiz response:', err);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
// Responder pregunta actual
|
||||
const answerQuestion = useCallback((questionId: string, selectedOptions: string[]) => {
|
||||
setCurrentAnswers(prev => {
|
||||
const existing = prev.findIndex(a => a.questionId === questionId);
|
||||
if (existing >= 0) {
|
||||
const updated = [...prev];
|
||||
updated[existing] = { questionId, selectedOptions };
|
||||
return updated;
|
||||
}
|
||||
return [...prev, { questionId, selectedOptions }];
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Ir a siguiente pregunta
|
||||
const nextQuestion = useCallback(() => {
|
||||
if (currentQuestionIndex < questions.length - 1) {
|
||||
setCurrentQuestionIndex(prev => prev + 1);
|
||||
}
|
||||
}, [currentQuestionIndex, questions.length]);
|
||||
|
||||
// Ir a pregunta anterior
|
||||
const prevQuestion = useCallback(() => {
|
||||
if (currentQuestionIndex > 0) {
|
||||
setCurrentQuestionIndex(prev => prev - 1);
|
||||
}
|
||||
}, [currentQuestionIndex]);
|
||||
|
||||
// Ir a pregunta específica
|
||||
const goToQuestion = useCallback((index: number) => {
|
||||
if (index >= 0 && index < questions.length) {
|
||||
setCurrentQuestionIndex(index);
|
||||
}
|
||||
}, [questions.length]);
|
||||
|
||||
// Enviar quiz
|
||||
const submitQuiz = useCallback(async (): Promise<QuizResponse | null> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para completar el quiz');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verificar que todas las preguntas estén respondidas
|
||||
const unanswered = questions.filter(
|
||||
q => !currentAnswers.find(a => a.questionId === q.id)
|
||||
);
|
||||
|
||||
if (unanswered.length > 0) {
|
||||
toast.error(`Faltan ${unanswered.length} preguntas por responder`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const response = await quizApi.submitQuiz({ answers: currentAnswers });
|
||||
setQuizResponse(response);
|
||||
toast.success(`¡Quiz completado! Tu Travel Persona es: ${response.travelPersona}`);
|
||||
return response;
|
||||
} catch (err) {
|
||||
console.error('Error submitting quiz:', err);
|
||||
toast.error('Error al enviar quiz');
|
||||
return null;
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}, [user?.id, questions, currentAnswers]);
|
||||
|
||||
// Reiniciar quiz
|
||||
const resetQuiz = useCallback(async (): Promise<boolean> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para reiniciar el quiz');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await quizApi.resetQuiz();
|
||||
setQuizResponse(null);
|
||||
setCurrentAnswers([]);
|
||||
setCurrentQuestionIndex(0);
|
||||
toast.success('Quiz reiniciado');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error resetting quiz:', err);
|
||||
toast.error('Error al reiniciar quiz');
|
||||
return false;
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
// Verificar si el quiz está completado
|
||||
const isCompleted = useCallback((): boolean => {
|
||||
return quizResponse?.isCompleted ?? false;
|
||||
}, [quizResponse]);
|
||||
|
||||
// Obtener la Travel Persona
|
||||
const getTravelPersona = useCallback(() => {
|
||||
if (quizResponse?.isCompleted && quizResponse.travelPersona) {
|
||||
return {
|
||||
persona: quizResponse.travelPersona,
|
||||
description: quizResponse.personaDescription,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [quizResponse]);
|
||||
|
||||
// Obtener respuesta de una pregunta
|
||||
const getAnswer = useCallback((questionId: string): string[] | undefined => {
|
||||
return currentAnswers.find(a => a.questionId === questionId)?.selectedOptions;
|
||||
}, [currentAnswers]);
|
||||
|
||||
// Verificar si una pregunta está respondida
|
||||
const isAnswered = useCallback((questionId: string): boolean => {
|
||||
return currentAnswers.some(a => a.questionId === questionId && a.selectedOptions.length > 0);
|
||||
}, [currentAnswers]);
|
||||
|
||||
// Obtener progreso del quiz
|
||||
const getProgress = useCallback(() => {
|
||||
const answered = currentAnswers.filter(a => a.selectedOptions.length > 0).length;
|
||||
return {
|
||||
answered,
|
||||
total: questions.length,
|
||||
percentage: questions.length > 0 ? Math.round((answered / questions.length) * 100) : 0,
|
||||
};
|
||||
}, [currentAnswers, questions.length]);
|
||||
|
||||
// Limpiar error
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Pregunta actual
|
||||
const currentQuestion = questions[currentQuestionIndex] || null;
|
||||
|
||||
// Carga inicial
|
||||
useEffect(() => {
|
||||
loadQuestions();
|
||||
if (user?.id) {
|
||||
loadMyResponse();
|
||||
}
|
||||
}, [loadQuestions, user?.id, loadMyResponse]);
|
||||
|
||||
return {
|
||||
questions,
|
||||
quizResponse,
|
||||
currentQuestion,
|
||||
currentQuestionIndex,
|
||||
currentAnswers,
|
||||
loading,
|
||||
submitting,
|
||||
error,
|
||||
loadQuestions,
|
||||
loadMyResponse,
|
||||
answerQuestion,
|
||||
nextQuestion,
|
||||
prevQuestion,
|
||||
goToQuestion,
|
||||
submitQuiz,
|
||||
resetQuiz,
|
||||
isCompleted,
|
||||
getTravelPersona,
|
||||
getAnswer,
|
||||
isAnswered,
|
||||
getProgress,
|
||||
clearError,
|
||||
isFirstQuestion: currentQuestionIndex === 0,
|
||||
isLastQuestion: currentQuestionIndex === questions.length - 1,
|
||||
};
|
||||
};
|
||||
|
||||
export default useQuiz;
|
||||
289
src/hooks/useTrips.ts
Normal file
289
src/hooks/useTrips.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { tripsApi, Trip, TripStatus, TripStats, TripDay, TripActivity, CreateTripDto, UpdateTripDto, CreateTripDayDto, UpdateTripDayDto, CreateTripActivityDto, UpdateTripActivityDto } from '@/services/tripsApi';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useTrips = (initialStatus?: TripStatus) => {
|
||||
const { user } = useAuth();
|
||||
const [trips, setTrips] = useState<Trip[]>([]);
|
||||
const [stats, setStats] = useState<TripStats | null>(null);
|
||||
const [selectedTrip, setSelectedTrip] = useState<Trip | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedStatus, setSelectedStatus] = useState<TripStatus | undefined>(initialStatus);
|
||||
|
||||
// Cargar viajes
|
||||
const loadTrips = useCallback(async (status?: TripStatus) => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await tripsApi.getMyTrips(status || selectedStatus);
|
||||
setTrips(data);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Error al cargar viajes';
|
||||
setError(errorMessage);
|
||||
console.error('Error loading trips:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user?.id, selectedStatus]);
|
||||
|
||||
// Cargar estadísticas
|
||||
const loadStats = useCallback(async () => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
const data = await tripsApi.getTripsStats();
|
||||
setStats(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading trips stats:', err);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
// Crear viaje
|
||||
const createTrip = useCallback(async (data: CreateTripDto): Promise<Trip | null> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para crear viajes');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const newTrip = await tripsApi.createTrip(data);
|
||||
toast.success('Viaje creado');
|
||||
await loadTrips();
|
||||
await loadStats();
|
||||
return newTrip;
|
||||
} catch (err) {
|
||||
console.error('Error creating trip:', err);
|
||||
toast.error('Error al crear viaje');
|
||||
return null;
|
||||
}
|
||||
}, [user?.id, loadTrips, loadStats]);
|
||||
|
||||
// Obtener viaje por ID
|
||||
const getTripById = useCallback(async (id: string): Promise<Trip | null> => {
|
||||
try {
|
||||
const trip = await tripsApi.getTripById(id);
|
||||
setSelectedTrip(trip);
|
||||
return trip;
|
||||
} catch (err) {
|
||||
console.error('Error fetching trip:', err);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Actualizar viaje
|
||||
const updateTrip = useCallback(async (id: string, data: UpdateTripDto): Promise<boolean> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para actualizar viajes');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await tripsApi.updateTrip(id, data);
|
||||
toast.success('Viaje actualizado');
|
||||
if (selectedTrip?.id === id) {
|
||||
setSelectedTrip(updated);
|
||||
}
|
||||
await loadTrips();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error updating trip:', err);
|
||||
toast.error('Error al actualizar viaje');
|
||||
return false;
|
||||
}
|
||||
}, [user?.id, selectedTrip?.id, loadTrips]);
|
||||
|
||||
// Eliminar viaje
|
||||
const deleteTrip = useCallback(async (id: string): Promise<boolean> => {
|
||||
if (!user?.id) {
|
||||
toast.error('Inicia sesión para eliminar viajes');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await tripsApi.deleteTrip(id);
|
||||
toast.success('Viaje eliminado');
|
||||
setTrips(prev => prev.filter(t => t.id !== id));
|
||||
if (selectedTrip?.id === id) {
|
||||
setSelectedTrip(null);
|
||||
}
|
||||
await loadStats();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error deleting trip:', err);
|
||||
toast.error('Error al eliminar viaje');
|
||||
return false;
|
||||
}
|
||||
}, [user?.id, selectedTrip?.id, loadStats]);
|
||||
|
||||
// ============ DAYS ============
|
||||
|
||||
// Agregar día
|
||||
const addDay = useCallback(async (tripId: string, data: CreateTripDayDto): Promise<TripDay | null> => {
|
||||
try {
|
||||
const newDay = await tripsApi.addDay(tripId, data);
|
||||
toast.success('Día agregado');
|
||||
if (selectedTrip?.id === tripId) {
|
||||
await getTripById(tripId);
|
||||
}
|
||||
return newDay;
|
||||
} catch (err) {
|
||||
console.error('Error adding day:', err);
|
||||
toast.error('Error al agregar día');
|
||||
return null;
|
||||
}
|
||||
}, [selectedTrip?.id, getTripById]);
|
||||
|
||||
// Actualizar día
|
||||
const updateDay = useCallback(async (tripId: string, dayId: string, data: UpdateTripDayDto): Promise<boolean> => {
|
||||
try {
|
||||
await tripsApi.updateDay(tripId, dayId, data);
|
||||
toast.success('Día actualizado');
|
||||
if (selectedTrip?.id === tripId) {
|
||||
await getTripById(tripId);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error updating day:', err);
|
||||
toast.error('Error al actualizar día');
|
||||
return false;
|
||||
}
|
||||
}, [selectedTrip?.id, getTripById]);
|
||||
|
||||
// Eliminar día
|
||||
const deleteDay = useCallback(async (tripId: string, dayId: string): Promise<boolean> => {
|
||||
try {
|
||||
await tripsApi.deleteDay(tripId, dayId);
|
||||
toast.success('Día eliminado');
|
||||
if (selectedTrip?.id === tripId) {
|
||||
await getTripById(tripId);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error deleting day:', err);
|
||||
toast.error('Error al eliminar día');
|
||||
return false;
|
||||
}
|
||||
}, [selectedTrip?.id, getTripById]);
|
||||
|
||||
// ============ ACTIVITIES ============
|
||||
|
||||
// Agregar actividad
|
||||
const addActivity = useCallback(async (tripId: string, dayId: string, data: CreateTripActivityDto): Promise<TripActivity | null> => {
|
||||
try {
|
||||
const newActivity = await tripsApi.addActivity(tripId, dayId, data);
|
||||
toast.success('Actividad agregada');
|
||||
if (selectedTrip?.id === tripId) {
|
||||
await getTripById(tripId);
|
||||
}
|
||||
return newActivity;
|
||||
} catch (err) {
|
||||
console.error('Error adding activity:', err);
|
||||
toast.error('Error al agregar actividad');
|
||||
return null;
|
||||
}
|
||||
}, [selectedTrip?.id, getTripById]);
|
||||
|
||||
// Actualizar actividad
|
||||
const updateActivity = useCallback(async (tripId: string, dayId: string, activityId: string, data: UpdateTripActivityDto): Promise<boolean> => {
|
||||
try {
|
||||
await tripsApi.updateActivity(tripId, dayId, activityId, data);
|
||||
toast.success('Actividad actualizada');
|
||||
if (selectedTrip?.id === tripId) {
|
||||
await getTripById(tripId);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error updating activity:', err);
|
||||
toast.error('Error al actualizar actividad');
|
||||
return false;
|
||||
}
|
||||
}, [selectedTrip?.id, getTripById]);
|
||||
|
||||
// Eliminar actividad
|
||||
const deleteActivity = useCallback(async (tripId: string, dayId: string, activityId: string): Promise<boolean> => {
|
||||
try {
|
||||
await tripsApi.deleteActivity(tripId, dayId, activityId);
|
||||
toast.success('Actividad eliminada');
|
||||
if (selectedTrip?.id === tripId) {
|
||||
await getTripById(tripId);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error deleting activity:', err);
|
||||
toast.error('Error al eliminar actividad');
|
||||
return false;
|
||||
}
|
||||
}, [selectedTrip?.id, getTripById]);
|
||||
|
||||
// Reordenar actividades
|
||||
const reorderActivities = useCallback(async (tripId: string, dayId: string, activityIds: string[]): Promise<boolean> => {
|
||||
try {
|
||||
await tripsApi.reorderActivities(tripId, dayId, activityIds);
|
||||
if (selectedTrip?.id === tripId) {
|
||||
await getTripById(tripId);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error reordering activities:', err);
|
||||
toast.error('Error al reordenar actividades');
|
||||
return false;
|
||||
}
|
||||
}, [selectedTrip?.id, getTripById]);
|
||||
|
||||
// Cambiar estado seleccionado
|
||||
const changeStatus = useCallback((status?: TripStatus) => {
|
||||
setSelectedStatus(status);
|
||||
loadTrips(status);
|
||||
}, [loadTrips]);
|
||||
|
||||
// Filtrar por estado
|
||||
const filterByStatus = useCallback((status: TripStatus): Trip[] => {
|
||||
return trips.filter(t => t.status === status);
|
||||
}, [trips]);
|
||||
|
||||
// Limpiar error
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Carga inicial
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
loadTrips();
|
||||
loadStats();
|
||||
}
|
||||
}, [user?.id, loadTrips, loadStats]);
|
||||
|
||||
return {
|
||||
trips,
|
||||
stats,
|
||||
selectedTrip,
|
||||
loading,
|
||||
error,
|
||||
selectedStatus,
|
||||
loadTrips,
|
||||
loadStats,
|
||||
createTrip,
|
||||
getTripById,
|
||||
updateTrip,
|
||||
deleteTrip,
|
||||
addDay,
|
||||
updateDay,
|
||||
deleteDay,
|
||||
addActivity,
|
||||
updateActivity,
|
||||
deleteActivity,
|
||||
reorderActivities,
|
||||
changeStatus,
|
||||
filterByStatus,
|
||||
setSelectedTrip,
|
||||
clearError,
|
||||
getTripsCount: () => trips.length,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTrips;
|
||||
Reference in New Issue
Block a user