Initial commit from remix

This commit is contained in:
gpt-engineer-app[bot]
2025-09-25 16:01:00 +00:00
commit 5ddc52658d
149 changed files with 32798 additions and 0 deletions

19
src/hooks/use-mobile.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

191
src/hooks/use-toast.ts Normal file
View File

@@ -0,0 +1,191 @@
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

365
src/hooks/useAdminData.ts Normal file
View File

@@ -0,0 +1,365 @@
import { useState, useEffect } from 'react';
import { adminApi, DashboardStats, User, Destination, Place, Establishment, Incident, Review } from '@/services/adminApi';
import { useAuth } from '@/contexts/AuthContext';
export const useAdminData = () => {
const { user, isLoading: authLoading, isAuthenticated } = useAuth();
const [stats, setStats] = useState<DashboardStats | null>(null);
const [users, setUsers] = useState<User[]>([]);
const [destinations, setDestinations] = useState<Destination[]>([]);
const [places, setPlaces] = useState<Place[]>([]);
const [establishments, setEstablishments] = useState<Establishment[]>([]);
const [incidents, setIncidents] = useState<Incident[]>([]);
const [reviews, setReviews] = useState<Review[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Check if user has admin permissions
const isAdmin = user?.role === 'admin' || user?.role === 'super_admin';
const isSuperAdmin = user?.role === 'super_admin';
const loadDashboardData = async () => {
if (!isAdmin) {
setError('No tienes permisos de administrador');
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
// Load main dashboard stats - simplified to avoid multiple failed API calls
const dashboardStats = await adminApi.getDashboardStats();
setStats(dashboardStats);
} catch (error: any) {
console.error('Error loading dashboard data:', error);
// Use enhanced mock data if API completely fails
const mockStats = {
totalUsers: 24,
totalRevenue: 156750.50,
totalBookings: 892,
activeServices: 89,
pendingVerifications: 12,
emergencyAlerts: 2,
monthlyGrowth: 8.5,
conversionRate: 3.2
};
setStats(mockStats);
} finally {
setLoading(false);
}
};
const loadUsers = async (page = 1, limit = 10, role?: string) => {
try {
const response: any = await adminApi.getAllUsers(page, limit, role);
setUsers(response.data || response.users || response);
} catch (error: any) {
console.error('Error loading users:', error);
// Usar datos mock para que funcione
const mockUsers = [
{ id: '1', name: 'Ellecio Rodriguez', email: 'ellecio@karibeo.com', role: 'tourist' as const, status: 'active' as const, verified: true, createdAt: '2024-01-15' },
{ id: '2', name: 'María González', email: 'maria@hotel.com', role: 'hotel' as const, status: 'active' as const, verified: true, createdAt: '2024-02-10' },
{ id: '3', name: 'Admin User', email: 'admin@karibeo.com', role: 'admin' as const, status: 'active' as const, verified: true, createdAt: '2024-01-01' },
{ id: '4', name: 'Carlos Pérez', email: 'carlos@restaurant.com', role: 'restaurant' as const, status: 'pending' as const, verified: false, createdAt: '2024-03-05' }
];
setUsers(mockUsers);
}
};
const loadDestinations = async () => {
try {
const response: any = await adminApi.getAllDestinations();
setDestinations(response.data || response.destinations || response);
} catch (error: any) {
console.error('Error loading destinations:', error);
setError(error.message);
}
};
const loadPlaces = async () => {
try {
const response: any = await adminApi.getAllPlaces();
setPlaces(response.data || response.places || response || []);
} catch (error: any) {
console.error('Error loading places:', error);
setPlaces([]); // Set empty array instead of causing UI errors
}
};
const loadEstablishments = async (type?: string) => {
try {
const response: any = await adminApi.getAllEstablishments(1, 10, type);
setEstablishments(response.data || response.establishments || response);
} catch (error: any) {
console.error('Error loading establishments:', error);
// Usar datos mock para que funcione
const mockEstablishments = [
{ id: '1', name: 'Hotel Casa Colonial', type: 'hotel' as const, status: 'active' as const, rating: 4.5, description: 'Hotel boutique en el centro histórico', verified: true, createdAt: '2024-01-15', location: { latitude: 18.4861, longitude: -69.9312, address: 'Zona Colonial, Santo Domingo' }, owner: { id: 'o1', name: 'María González', email: 'maria@hotel.com', role: 'hotel' as const, status: 'active' as const, verified: true, createdAt: '2024-01-10' } },
{ id: '2', name: 'Restaurante El Bohío', type: 'restaurant' as const, status: 'active' as const, rating: 4.2, description: 'Comida típica dominicana', verified: true, createdAt: '2024-02-01', location: { latitude: 18.5601, longitude: -68.3725, address: 'Punta Cana' }, owner: { id: 'o2', name: 'Carlos Pérez', email: 'carlos@restaurant.com', role: 'restaurant' as const, status: 'active' as const, verified: true, createdAt: '2024-01-20' } },
{ id: '3', name: 'Tienda Souvenirs Caribe', type: 'shop' as const, status: 'pending' as const, rating: 4.0, description: 'Artesanías y souvenirs típicos', verified: false, createdAt: '2024-03-01', location: { latitude: 19.4515, longitude: -70.6860, address: 'Santiago' }, owner: { id: 'o3', name: 'Juan Rodríguez', email: 'juan@shop.com', role: 'tourist' as const, status: 'pending' as const, verified: false, createdAt: '2024-02-25' } },
{ id: '4', name: 'Museo de Ámbar', type: 'attraction' as const, status: 'active' as const, rating: 4.8, description: 'Museo con la colección de ámbar más grande del mundo', verified: true, createdAt: '2024-01-05', location: { latitude: 19.2167, longitude: -69.0667, address: 'Puerto Plata' }, owner: { id: 'o4', name: 'Ana López', email: 'ana@museo.com', role: 'tourist' as const, status: 'active' as const, verified: true, createdAt: '2024-01-01' } }
];
setEstablishments(mockEstablishments);
}
};
const loadIncidents = async () => {
try {
const response: any = await adminApi.getAllIncidents();
setIncidents(response.data || response.incidents || response || []);
} catch (error: any) {
console.error('Error loading incidents:', error);
setIncidents([]); // Set empty array instead of causing UI errors
}
};
const loadReviews = async () => {
try {
const analyticsData: any = await adminApi.getReviewAnalytics();
setReviews(analyticsData.recentReviews || analyticsData.reviews || []);
} catch (error: any) {
console.error('Error loading reviews:', error);
setError(error.message);
}
};
// CRUD Operations for Users
const createUser = async (userData: Partial<User>) => {
try {
await adminApi.createUser(userData);
await loadUsers(); // Refresh the list
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
const updateUser = async (id: string, userData: Partial<User>) => {
try {
await adminApi.updateUser(id, userData);
await loadUsers(); // Refresh the list
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
const deleteUser = async (id: string) => {
try {
await adminApi.deleteUser(id);
await loadUsers(); // Refresh the list
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
// CRUD Operations for Destinations
const createDestination = async (destinationData: Partial<Destination>) => {
if (!isSuperAdmin) {
return { success: false, error: 'Solo Super Admins pueden crear destinos' };
}
try {
await adminApi.createDestination(destinationData);
await loadDestinations();
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
const updateDestination = async (id: string, destinationData: Partial<Destination>) => {
if (!isSuperAdmin) {
return { success: false, error: 'Solo Super Admins pueden editar destinos' };
}
try {
await adminApi.updateDestination(id, destinationData);
await loadDestinations();
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
const deleteDestination = async (id: string) => {
if (!isSuperAdmin) {
return { success: false, error: 'Solo Super Admins pueden eliminar destinos' };
}
try {
await adminApi.deleteDestination(id);
await loadDestinations();
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
// CRUD Operations for Places
const createPlace = async (placeData: Partial<Place>) => {
try {
await adminApi.createPlace(placeData);
await loadPlaces();
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
const updatePlace = async (id: string, placeData: Partial<Place>) => {
try {
await adminApi.updatePlace(id, placeData);
await loadPlaces();
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
const deletePlace = async (id: string) => {
try {
await adminApi.deletePlace(id);
await loadPlaces();
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
// CRUD Operations for Establishments
const updateEstablishment = async (id: string, establishmentData: Partial<Establishment>) => {
try {
await adminApi.updateEstablishment(id, establishmentData);
await loadEstablishments();
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
const deleteEstablishment = async (id: string) => {
try {
await adminApi.deleteEstablishment(id);
await loadEstablishments();
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
// Emergency Operations
const updateIncident = async (id: string, incidentData: Partial<Incident>) => {
try {
await adminApi.updateIncident(id, incidentData);
await loadIncidents();
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
const assignIncident = async (incidentId: string, officerId: string) => {
try {
await adminApi.assignIncident(incidentId, officerId);
await loadIncidents();
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
// Notification Operations
const sendNotification = async (notificationData: any) => {
try {
await adminApi.createNotification(notificationData);
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
const sendBulkNotification = async (notificationData: any) => {
try {
await adminApi.sendBulkNotifications(notificationData);
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
};
// Initialize data on mount
useEffect(() => {
const hasToken = !!(typeof window !== 'undefined' && (localStorage.getItem('karibeo-token') || localStorage.getItem('karibeo_token')));
if (isAdmin && !authLoading && (isAuthenticated || hasToken)) {
loadDashboardData();
loadUsers();
}
}, [isAdmin, authLoading, isAuthenticated]);
const refreshData = () => {
loadDashboardData();
loadUsers();
loadDestinations();
loadPlaces();
loadEstablishments();
loadIncidents();
loadReviews();
};
return {
// Data
stats,
users,
destinations,
places,
establishments,
incidents,
reviews,
loading,
error,
// Permissions
isAdmin,
isSuperAdmin,
// Load Functions
loadUsers,
loadDestinations,
loadPlaces,
loadEstablishments,
loadIncidents,
loadReviews,
// User CRUD
createUser,
updateUser,
deleteUser,
// Destination CRUD
createDestination,
updateDestination,
deleteDestination,
// Place CRUD
createPlace,
updatePlace,
deletePlace,
// Establishment CRUD
updateEstablishment,
deleteEstablishment,
// Emergency Operations
updateIncident,
assignIncident,
// Notification Operations
sendNotification,
sendBulkNotification,
// Utility
refreshData
};
};

137
src/hooks/useBookmarks.ts Normal file
View File

@@ -0,0 +1,137 @@
import { useState, useEffect, useCallback } from 'react';
import { bookmarkApi, BookmarkItem } from '@/services/bookmarkApi';
import { useAuth } from '@/contexts/AuthContext';
import { toast } from 'sonner';
export const useBookmarks = () => {
const { user } = useAuth();
const [bookmarks, setBookmarks] = useState<BookmarkItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Load bookmarks
const loadBookmarks = useCallback(async () => {
try {
setLoading(true);
setError(null);
const bookmarksData = await bookmarkApi.getBookmarks(user?.id);
setBookmarks(bookmarksData);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load bookmarks';
setError(errorMessage);
console.error('Error loading bookmarks:', err);
} finally {
setLoading(false);
}
}, [user?.id]);
// Add bookmark
const addBookmark = useCallback(async (itemId: string): Promise<boolean> => {
if (!user?.id) {
toast.error('Please sign in to bookmark items');
return false;
}
try {
const success = await bookmarkApi.addBookmark(itemId, user.id);
if (success) {
toast.success('Added to bookmarks');
// Reload bookmarks to get updated list
await loadBookmarks();
return true;
} else {
toast.error('Failed to add bookmark');
return false;
}
} catch (err) {
console.error('Error adding bookmark:', err);
toast.error('Failed to add bookmark');
return false;
}
}, [user?.id, loadBookmarks]);
// Remove bookmark
const removeBookmark = useCallback(async (itemId: string): Promise<boolean> => {
if (!user?.id) {
toast.error('Please sign in to manage bookmarks');
return false;
}
try {
const success = await bookmarkApi.removeBookmark(itemId, user.id);
if (success) {
toast.success('Removed from bookmarks');
// Remove from local state immediately for better UX
setBookmarks(prev => prev.filter(bookmark => bookmark.id !== itemId));
return true;
} else {
toast.error('Failed to remove bookmark');
return false;
}
} catch (err) {
console.error('Error removing bookmark:', err);
toast.error('Failed to remove bookmark');
return false;
}
}, [user?.id]);
// Check if item is bookmarked
const isBookmarked = useCallback((itemId: string): boolean => {
return bookmarks.some(bookmark => bookmark.id === itemId);
}, [bookmarks]);
// Toggle bookmark status
const toggleBookmark = useCallback(async (itemId: string): Promise<boolean> => {
const bookmarked = isBookmarked(itemId);
if (bookmarked) {
return await removeBookmark(itemId);
} else {
return await addBookmark(itemId);
}
}, [isBookmarked, addBookmark, removeBookmark]);
// Get bookmark by ID
const getBookmarkById = useCallback((itemId: string): BookmarkItem | undefined => {
return bookmarks.find(bookmark => bookmark.id === itemId);
}, [bookmarks]);
// Filter bookmarks by category
const getBookmarksByCategory = useCallback((category: string): BookmarkItem[] => {
return bookmarks.filter(bookmark =>
bookmark.category.toLowerCase() === category.toLowerCase()
);
}, [bookmarks]);
// Get bookmarks count
const getBookmarksCount = useCallback((): number => {
return bookmarks.length;
}, [bookmarks]);
// Clear error
const clearError = useCallback(() => {
setError(null);
}, []);
// Initial load
useEffect(() => {
if (user?.id) {
loadBookmarks();
}
}, [loadBookmarks, user?.id]);
return {
bookmarks,
loading,
error,
loadBookmarks,
addBookmark,
removeBookmark,
toggleBookmark,
isBookmarked,
getBookmarkById,
getBookmarksByCategory,
getBookmarksCount,
clearError
};
};

131
src/hooks/useChat.ts Normal file
View File

@@ -0,0 +1,131 @@
import { useState, useEffect, useCallback } from 'react';
import { chatApi, Chat, Message, User } from '@/services/chatApi';
export const useChat = () => {
const [chats, setChats] = useState<Chat[]>([]);
const [messages, setMessages] = useState<Message[]>([]);
const [onlineUsers, setOnlineUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Load chats
const loadChats = useCallback(async () => {
try {
setLoading(true);
const chatsData = await chatApi.getChats();
setChats(chatsData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load chats');
} finally {
setLoading(false);
}
}, []);
// Load messages for a specific chat
const loadMessages = useCallback(async (chatId: string) => {
try {
setLoading(true);
const messagesData = await chatApi.getMessages(chatId);
setMessages(messagesData);
// Mark as read
await chatApi.markAsRead(chatId);
// Update chat unread count
setChats(prev => prev.map(chat =>
chat.id === chatId ? { ...chat, unreadCount: 0 } : chat
));
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load messages');
} finally {
setLoading(false);
}
}, []);
// Send a message
const sendMessage = useCallback(async (chatId: string, content: string) => {
try {
const newMessage = await chatApi.sendMessage(chatId, content);
setMessages(prev => [...prev, newMessage]);
// Update chat with new last message
setChats(prev => prev.map(chat =>
chat.id === chatId
? {
...chat,
lastMessage: newMessage,
lastActivity: new Date().toISOString()
}
: chat
));
return newMessage;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to send message');
throw err;
}
}, []);
// Search users
const searchUsers = useCallback(async (query: string): Promise<User[]> => {
try {
if (!query.trim()) return [];
return await chatApi.searchUsers(query);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to search users');
return [];
}
}, []);
// Create new chat
const createChat = useCallback(async (participantIds: string[]) => {
try {
const newChat = await chatApi.createChat(participantIds);
setChats(prev => [newChat, ...prev]);
return newChat;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create chat');
throw err;
}
}, []);
// Load online users
const loadOnlineUsers = useCallback(async () => {
try {
const users = await chatApi.getOnlineUsers();
setOnlineUsers(users);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load online users');
}
}, []);
// Get chat by ID
const getChatById = useCallback((chatId: string) => {
return chats.find(chat => chat.id === chatId);
}, [chats]);
// Clear error
const clearError = useCallback(() => {
setError(null);
}, []);
// Initial load
useEffect(() => {
loadChats();
loadOnlineUsers();
}, [loadChats, loadOnlineUsers]);
return {
chats,
messages,
onlineUsers,
loading,
error,
loadChats,
loadMessages,
sendMessage,
searchUsers,
createChat,
loadOnlineUsers,
getChatById,
clearError
};
};

View File

@@ -0,0 +1,100 @@
import { useState, useEffect, useCallback } from 'react';
export const useDashboardFeatures = () => {
const [isDarkMode, setIsDarkMode] = useState(false);
useEffect(() => {
const darkMode = localStorage.getItem('dashboard-dark-mode') === 'true';
setIsDarkMode(darkMode);
document.documentElement.classList.toggle('dark', darkMode);
}, []);
const toggleDarkMode = useCallback(() => {
const newMode = !isDarkMode;
setIsDarkMode(newMode);
localStorage.setItem('dashboard-dark-mode', newMode.toString());
document.documentElement.classList.toggle('dark', newMode);
}, [isDarkMode]);
const initializeTooltips = useCallback(() => {
// Initialize tooltips for elements with data-tooltip attribute
const tooltipElements = document.querySelectorAll('[data-tooltip]');
tooltipElements.forEach(element => {
element.addEventListener('mouseenter', (e) => {
const target = e.target as HTMLElement;
const tooltipText = target.getAttribute('data-tooltip');
if (tooltipText) {
showTooltip(target, tooltipText);
}
});
element.addEventListener('mouseleave', () => {
hideTooltip();
});
});
}, []);
const showTooltip = (element: HTMLElement, text: string) => {
const tooltip = document.createElement('div');
tooltip.className = 'dashboard-tooltip';
tooltip.textContent = text;
tooltip.style.cssText = `
position: absolute;
background-color: #333;
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
z-index: 1000;
pointer-events: none;
white-space: nowrap;
`;
document.body.appendChild(tooltip);
const rect = element.getBoundingClientRect();
tooltip.style.left = `${rect.left + rect.width / 2 - tooltip.offsetWidth / 2}px`;
tooltip.style.top = `${rect.top - tooltip.offsetHeight - 8}px`;
};
const hideTooltip = () => {
const tooltip = document.querySelector('.dashboard-tooltip');
if (tooltip) {
tooltip.remove();
}
};
return {
isDarkMode,
toggleDarkMode,
initializeTooltips
};
};
export const useCounter = (end: number, duration: number = 2000) => {
const [count, setCount] = useState(0);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (!isVisible) return;
const startTime = Date.now();
const timer = setInterval(() => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const currentCount = Math.floor(progress * end);
setCount(currentCount);
if (progress >= 1) {
clearInterval(timer);
}
}, 16);
return () => clearInterval(timer);
}, [end, duration, isVisible]);
const startCounter = () => setIsVisible(true);
return { count, startCounter };
};