diff --git a/src/App.tsx b/src/App.tsx index 1f08dcd..ad78f0f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,6 +29,10 @@ import Messages from "./pages/dashboard/Messages"; import Reviews from "./pages/dashboard/Reviews"; import Bookings from "./pages/dashboard/Bookings"; import Bookmarks from "./pages/dashboard/Bookmarks"; +import Favorites from "./pages/dashboard/Favorites"; +import Collections from "./pages/dashboard/Collections"; +import TripsPage from "./pages/dashboard/Trips"; +import QuizPage from "./pages/dashboard/Quiz"; import Profile from "./pages/dashboard/Profile"; import Settings from "./pages/dashboard/Settings"; import Invoices from "./pages/dashboard/Invoices"; @@ -65,6 +69,8 @@ import CRMDashboard from "./pages/dashboard/crm/CRMDashboard"; import CRMContacts from "./pages/dashboard/crm/Contacts"; import CRMCampaigns from "./pages/dashboard/crm/Campaigns"; import CRMAnalytics from "./pages/dashboard/crm/Analytics"; +// Influencer pages +import InfluencerDashboard from "./pages/dashboard/influencer/InfluencerDashboard"; // Roles & Permissions import RolesPermissions from "./pages/dashboard/RolesPermissions"; // Tourist App @@ -269,7 +275,40 @@ const AppRouter = () => ( } /> - + + {/* Phase 1 Routes - Favorites, Collections, Trips, Quiz */} + + + + + + } /> + + + + + + + } /> + + + + + + + } /> + + + + + + + } /> + @@ -700,7 +739,48 @@ const AppRouter = () => ( } /> - + + {/* Influencer Dashboard Routes */} + + + + + + } /> + + + + + + + } /> + + + + + + + } /> + + + + + + + } /> + + + + + + + } /> + {/* Catch-all route */} } /> diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index 1e33d95..22860e0 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -5,19 +5,19 @@ import { useLanguage } from '@/contexts/LanguageContext'; import DashboardStyles from '@/components/layouts/DashboardStyles'; import CurrencySelector from '@/components/CurrencySelector'; import LanguageSelector from '@/components/LanguageSelector'; -import { - Home, - Plus, - Wallet, - List, - MessageSquare, - Star, - BookOpen, - Heart, - FileText, - User, +import { + Home, + Plus, + Wallet, + List, + MessageSquare, + Star, + BookOpen, + Heart, + FileText, + User, Users, - Settings, + Settings, LogOut, Search, Bell, @@ -60,7 +60,9 @@ import { Server, ShieldAlert, UserCircle, - Mail + Mail, + TrendingUp, + Instagram } from 'lucide-react'; const DashboardLayout = ({ children }: { children: React.ReactNode }) => { @@ -217,9 +219,9 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => { { icon: FileText, label: 'Reportes', path: '/dashboard/politur/reports' } ] }, - { - icon: UserCircle, - label: 'Guías Turísticos', + { + icon: UserCircle, + label: 'Guías Turísticos', path: '/dashboard/guides', subItems: [ { icon: Home, label: 'Dashboard', path: '/dashboard/guides' }, @@ -227,6 +229,18 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => { { icon: BookOpen, label: 'Biblioteca', path: '/dashboard/guides/library' } ] }, + { + icon: Instagram, + label: 'Influencer Dashboard', + path: '/dashboard/influencer', + subItems: [ + { icon: Home, label: 'Mi Dashboard', path: '/dashboard/influencer' }, + { icon: BarChart3, label: 'Mis Estadísticas', path: '/dashboard/influencer/stats' }, + { icon: Megaphone, label: 'Campañas', path: '/dashboard/influencer/campaigns' }, + { icon: TrendingUp, label: 'Earnings', path: '/dashboard/influencer/earnings' }, + { icon: Users, label: 'Mi Perfil Público', path: '/dashboard/influencer/profile' } + ] + }, { icon: Store, label: t('commerce'), diff --git a/src/components/InfluencerMarketplace.tsx b/src/components/InfluencerMarketplace.tsx new file mode 100644 index 0000000..dbd3844 --- /dev/null +++ b/src/components/InfluencerMarketplace.tsx @@ -0,0 +1,409 @@ +import { useState, useEffect } from "react"; +import { + Users, + Star, + Instagram, + Youtube, + Twitter, + MapPin, + TrendingUp, + Heart, + MessageCircle, + Filter, + Search, + Loader2, + BadgeCheck, + Globe, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; + +interface Influencer { + id: string; + userId: string; + username: string; + displayName: string; + avatar?: string; + bio?: string; + followers: number; + engagement: number; + categories: string[]; + platforms: string[]; + location?: string; + verified: boolean; + rating: number; + campaignsCompleted: number; + pricePerPost?: number; +} + +const categoryFilters = [ + { id: "all", label: "Todos" }, + { id: "travel", label: "Viajes" }, + { id: "food", label: "Gastronomía" }, + { id: "lifestyle", label: "Lifestyle" }, + { id: "adventure", label: "Aventura" }, + { id: "luxury", label: "Lujo" }, +]; + +const InfluencerMarketplace = () => { + const [influencers, setInfluencers] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedCategory, setSelectedCategory] = useState("all"); + const [favorites, setFavorites] = useState([]); + + useEffect(() => { + fetchInfluencers(); + }, [selectedCategory]); + + const fetchInfluencers = async () => { + setLoading(true); + try { + // TODO: Conectar con API real + // const params: Record = { page: 1, limit: 20 }; + // if (selectedCategory !== "all") { + // params.category = selectedCategory; + // } + // const result = await api.getInfluencerMarketplace(params); + // setInfluencers(result.influencers || result || []); + + // Mock data para demo + await new Promise(resolve => setTimeout(resolve, 500)); + setInfluencers([ + { + id: "1", + userId: "u1", + username: "maria_viajera", + displayName: "Maria Rodriguez", + avatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150", + bio: "Exploradora del Caribe. Comparto los mejores destinos y experiencias de viaje.", + followers: 125000, + engagement: 4.8, + categories: ["travel", "lifestyle"], + platforms: ["instagram", "youtube"], + location: "Santo Domingo, RD", + verified: true, + rating: 4.9, + campaignsCompleted: 45, + pricePerPost: 500, + }, + { + id: "2", + userId: "u2", + username: "chef_carlos", + displayName: "Carlos Mendez", + avatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150", + bio: "Chef y food blogger. Descubriendo la gastronomía dominicana.", + followers: 89000, + engagement: 5.2, + categories: ["food", "travel"], + platforms: ["instagram", "twitter"], + location: "Santiago, RD", + verified: true, + rating: 4.7, + campaignsCompleted: 32, + pricePerPost: 350, + }, + { + id: "3", + userId: "u3", + username: "adventure_rd", + displayName: "Pedro Adventures", + avatar: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=150", + bio: "Aventuras extremas en el Caribe. Rafting, parapente, buceo y más.", + followers: 67000, + engagement: 6.1, + categories: ["adventure", "travel"], + platforms: ["youtube", "instagram"], + location: "Jarabacoa, RD", + verified: false, + rating: 4.8, + campaignsCompleted: 18, + pricePerPost: 280, + }, + { + id: "4", + userId: "u4", + username: "luxury_caribbean", + displayName: "Ana Luxury Travel", + avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150", + bio: "Experiencias de lujo en el Caribe. Resorts, spas y gastronomía premium.", + followers: 210000, + engagement: 3.9, + categories: ["luxury", "lifestyle", "travel"], + platforms: ["instagram"], + location: "Punta Cana, RD", + verified: true, + rating: 4.6, + campaignsCompleted: 78, + pricePerPost: 1200, + }, + { + id: "5", + userId: "u5", + username: "rd_foodie", + displayName: "Laura Foodie", + avatar: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=150", + bio: "Descubriendo la mejor comida callejera y restaurantes de RD.", + followers: 45000, + engagement: 7.2, + categories: ["food", "lifestyle"], + platforms: ["instagram", "twitter"], + location: "Santo Domingo, RD", + verified: false, + rating: 4.5, + campaignsCompleted: 12, + pricePerPost: 200, + }, + { + id: "6", + userId: "u6", + username: "caribbean_surfer", + displayName: "Diego Surf", + avatar: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=150", + bio: "Surf, kitesurf y deportes acuáticos en las mejores playas del Caribe.", + followers: 156000, + engagement: 5.5, + categories: ["adventure", "lifestyle"], + platforms: ["youtube", "instagram"], + location: "Cabarete, RD", + verified: true, + rating: 4.8, + campaignsCompleted: 56, + pricePerPost: 650, + }, + ]); + } catch (error) { + console.error("Error fetching influencers:", error); + } finally { + setLoading(false); + } + }; + + const toggleFavorite = (id: string) => { + setFavorites((prev) => + prev.includes(id) ? prev.filter((f) => f !== id) : [...prev, id] + ); + }; + + const formatFollowers = (count: number) => { + if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`; + if (count >= 1000) return `${(count / 1000).toFixed(0)}K`; + return count.toString(); + }; + + const getPlatformIcon = (platform: string) => { + switch (platform.toLowerCase()) { + case "instagram": + return Instagram; + case "youtube": + return Youtube; + case "twitter": + return Twitter; + default: + return Globe; + } + }; + + const filteredInfluencers = influencers.filter((inf) => { + const matchesSearch = + inf.displayName.toLowerCase().includes(searchQuery.toLowerCase()) || + inf.username.toLowerCase().includes(searchQuery.toLowerCase()) || + inf.bio?.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesCategory = + selectedCategory === "all" || + inf.categories.includes(selectedCategory); + + return matchesSearch && matchesCategory; + }); + + return ( +
+ {/* Header */} +
+
+

+ + Marketplace de Influencers +

+

+ Conecta con creadores de contenido para promocionar tu destino +

+
+ +
+ + {/* Search & Filters */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ +
+ + {/* Category Filters */} +
+ {categoryFilters.map((cat) => ( + + ))} +
+ + {/* Loading */} + {loading && ( +
+ +
+ )} + + {/* Influencer Grid */} + {!loading && ( +
+ {filteredInfluencers.map((influencer) => ( +
+ {/* Header */} +
+
+ {influencer.displayName} + {influencer.verified && ( + + )} +
+
+
+

{influencer.displayName}

+
+

@{influencer.username}

+ {influencer.location && ( +
+ + {influencer.location} +
+ )} +
+ +
+ + {/* Bio */} + {influencer.bio && ( +

+ {influencer.bio} +

+ )} + + {/* Stats */} +
+
+

{formatFollowers(influencer.followers)}

+

Seguidores

+
+
+

{influencer.engagement}%

+

Engagement

+
+
+
+ + {influencer.rating} +
+

Rating

+
+
+ + {/* Platforms */} +
+ {influencer.platforms.map((platform) => { + const Icon = getPlatformIcon(platform); + return ( +
+ +
+ ); + })} +
+ {influencer.categories.slice(0, 2).map((cat) => ( + + {cat} + + ))} +
+ + {/* Footer */} +
+
+ {influencer.pricePerPost && ( +

+ ${influencer.pricePerPost} + /post +

+ )} +

+ {influencer.campaignsCompleted} campañas +

+
+ +
+
+ ))} +
+ )} + + {/* Empty State */} + {!loading && filteredInfluencers.length === 0 && ( +
+ +

No se encontraron influencers

+
+ )} +
+ ); +}; + +export default InfluencerMarketplace; diff --git a/src/config/api.ts b/src/config/api.ts index f9debc5..880abc7 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -1,48 +1,79 @@ // API Configuration and Constants export const API_CONFIG = { - BASE_URL: 'https://karibeo.lesoluciones.net:8443/api/v1', + BASE_URL: 'https://api.karibeo.ai:8443/api/v1', ENDPOINTS: { // Authentication LOGIN: '/auth/login', REGISTER: '/auth/register', REFRESH: '/auth/refresh', - + // Users USERS: '/users', USER_PROFILE: '/users/profile', UPDATE_USER: '/users/update', - + // Invoices INVOICES: '/invoices', INVOICE_DETAIL: '/invoices/:id', - + // Bookings BOOKINGS: '/bookings', - + // Wallet WALLET: '/wallet', TRANSACTIONS: '/wallet/transactions', - + // Commerce ESTABLISHMENTS: '/commerce/establishments', ESTABLISHMENT_DETAIL: '/commerce/establishments/:id', RESERVATIONS: '/commerce/reservations', RESERVATION_DETAIL: '/commerce/reservations/:id', COMMERCE_STATS: '/commerce/stats', - + // Restaurant MENU_ITEMS: '/restaurant/menu-items', RESTAURANT_MENU: '/restaurant/establishments/:id/menu', RESTAURANT_TABLES: '/restaurant/establishments/:id/tables', RESTAURANT_ORDERS: '/restaurant/establishments/:id/orders', RESTAURANT_STATS: '/restaurant/establishments/:id/stats', - + // Hotel HOTEL_ROOMS: '/hotel/establishments/:id/rooms', HOTEL_CHECKIN: '/hotel/checkin', HOTEL_ROOM_SERVICE: '/hotel/room-service', HOTEL_STATS: '/hotel/establishments/:id/stats', HOTEL_HOUSEKEEPING: '/hotel/establishments/:id/housekeeping', + + // Favorites (Fase 1) + FAVORITES: '/favorites', + FAVORITES_MY: '/favorites/my', + FAVORITES_COUNTS: '/favorites/my/counts', + FAVORITES_CHECK: '/favorites/check/:itemType/:itemId', + FAVORITES_TOGGLE: '/favorites/toggle', + + // Collections (Fase 1) + COLLECTIONS: '/collections', + COLLECTIONS_MY: '/collections/my', + COLLECTIONS_STATS: '/collections/my/stats', + COLLECTION_ITEMS: '/collections/:id/items', + COLLECTION_ITEMS_ORDER: '/collections/:id/items/order', + COLLECTIONS_ORDER: '/collections/order', + + // Trips (Fase 1) + TRIPS: '/trips', + TRIPS_MY: '/trips/my', + TRIPS_STATS: '/trips/my/stats', + TRIP_DAYS: '/trips/:tripId/days', + TRIP_DAY: '/trips/:tripId/days/:dayId', + TRIP_ACTIVITIES: '/trips/:tripId/days/:dayId/activities', + TRIP_ACTIVITY: '/trips/:tripId/days/:dayId/activities/:activityId', + TRIP_ACTIVITIES_ORDER: '/trips/:tripId/days/:dayId/activities/order', + + // Quiz (Fase 1) + QUIZ_QUESTIONS: '/quiz/questions', + QUIZ_MY: '/quiz/my', + QUIZ_SUBMIT: '/quiz/submit', + QUIZ_RESET: '/quiz/reset', }, // External Assets diff --git a/src/hooks/useCollections.ts b/src/hooks/useCollections.ts new file mode 100644 index 0000000..e749274 --- /dev/null +++ b/src/hooks/useCollections.ts @@ -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([]); + const [stats, setStats] = useState(null); + const [selectedCollection, setSelectedCollection] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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; diff --git a/src/hooks/useFavorites.ts b/src/hooks/useFavorites.ts new file mode 100644 index 0000000..a9b257c --- /dev/null +++ b/src/hooks/useFavorites.ts @@ -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([]); + const [counts, setCounts] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [selectedType, setSelectedType] = useState(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 => { + 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 => { + 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 => { + 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 => { + 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; diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts new file mode 100644 index 0000000..455757e --- /dev/null +++ b/src/hooks/useNotifications.ts @@ -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; + markAsRead: (notificationId: string) => Promise; + markAllAsRead: () => Promise; + deleteNotification: (notificationId: string) => Promise; + deleteAllRead: () => Promise; + refresh: () => Promise; +} + +export function useNotifications(options: UseNotificationsOptions = {}): UseNotificationsReturn { + const { autoFetch = true, pollingInterval = 0 } = options; + const { toast } = useToast(); + + const [notifications, setNotifications] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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; diff --git a/src/hooks/useQuiz.ts b/src/hooks/useQuiz.ts new file mode 100644 index 0000000..40a742c --- /dev/null +++ b/src/hooks/useQuiz.ts @@ -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([]); + const [quizResponse, setQuizResponse] = useState(null); + const [currentAnswers, setCurrentAnswers] = useState([]); + const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); + const [loading, setLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(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 => { + 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 => { + 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; diff --git a/src/hooks/useTrips.ts b/src/hooks/useTrips.ts new file mode 100644 index 0000000..c7bf4a3 --- /dev/null +++ b/src/hooks/useTrips.ts @@ -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([]); + const [stats, setStats] = useState(null); + const [selectedTrip, setSelectedTrip] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [selectedStatus, setSelectedStatus] = useState(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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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; diff --git a/src/pages/dashboard/Collections.tsx b/src/pages/dashboard/Collections.tsx new file mode 100644 index 0000000..29afb5e --- /dev/null +++ b/src/pages/dashboard/Collections.tsx @@ -0,0 +1,518 @@ +import { useState } from 'react'; +import { useCollections } from '@/hooks/useCollections'; +import { + FolderHeart, + Plus, + Trash2, + Edit2, + Eye, + Globe, + Lock, + RefreshCw, + Search, + MoreVertical, + Image as ImageIcon, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; + +const Collections = () => { + const { + collections, + stats, + loading, + error, + loadCollections, + createCollection, + updateCollection, + deleteCollection, + getCollectionById, + selectedCollection, + setSelectedCollection, + } = useCollections(); + + const [searchTerm, setSearchTerm] = useState(''); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [collectionToDelete, setCollectionToDelete] = useState(null); + + // Form state + const [formData, setFormData] = useState({ + name: '', + description: '', + coverImageUrl: '', + color: '#3b82f6', + isPublic: false, + }); + + const filteredCollections = collections.filter(col => + col.name.toLowerCase().includes(searchTerm.toLowerCase()) || + col.description?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleRefresh = async () => { + await loadCollections(); + }; + + const handleCreate = async () => { + const result = await createCollection(formData); + if (result) { + setIsCreateDialogOpen(false); + resetForm(); + } + }; + + const handleEdit = async () => { + if (!selectedCollection) return; + const success = await updateCollection(selectedCollection.id, formData); + if (success) { + setIsEditDialogOpen(false); + resetForm(); + } + }; + + const handleDelete = async () => { + if (!collectionToDelete) return; + const success = await deleteCollection(collectionToDelete); + if (success) { + setIsDeleteDialogOpen(false); + setCollectionToDelete(null); + } + }; + + const openEditDialog = async (collectionId: string) => { + const collection = await getCollectionById(collectionId); + if (collection) { + setFormData({ + name: collection.name, + description: collection.description || '', + coverImageUrl: collection.coverImageUrl || '', + color: collection.color || '#3b82f6', + isPublic: collection.isPublic, + }); + setIsEditDialogOpen(true); + } + }; + + const openDeleteDialog = (collectionId: string) => { + setCollectionToDelete(collectionId); + setIsDeleteDialogOpen(true); + }; + + const resetForm = () => { + setFormData({ + name: '', + description: '', + coverImageUrl: '', + color: '#3b82f6', + isPublic: false, + }); + setSelectedCollection(null); + }; + + if (loading && collections.length === 0) { + return ( +
+
+
+
+
+

Cargando colecciones...

+
+
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+
+

+ + Gestión de Colecciones +

+

+ Administra las colecciones de lugares favoritos +

+
+
+ + + + + + + + Crear Colección + + Crea una nueva colección para organizar lugares. + + +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Mi colección" + /> +
+
+ +