192 lines
4.9 KiB
TypeScript
192 lines
4.9 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from "react";
|
|
|
|
/**
|
|
* Hook to detect if the user is online/offline
|
|
*/
|
|
export function useOnlineStatus(): boolean {
|
|
const [isOnline, setIsOnline] = useState(
|
|
typeof navigator !== "undefined" ? navigator.onLine : true
|
|
);
|
|
|
|
useEffect(() => {
|
|
const handleOnline = () => setIsOnline(true);
|
|
const handleOffline = () => setIsOnline(false);
|
|
|
|
window.addEventListener("online", handleOnline);
|
|
window.addEventListener("offline", handleOffline);
|
|
|
|
return () => {
|
|
window.removeEventListener("online", handleOnline);
|
|
window.removeEventListener("offline", handleOffline);
|
|
};
|
|
}, []);
|
|
|
|
return isOnline;
|
|
}
|
|
|
|
/**
|
|
* Hook to debounce a value
|
|
*/
|
|
export function useDebounce<T>(value: T, delay: number): T {
|
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
setDebouncedValue(value);
|
|
}, delay);
|
|
|
|
return () => {
|
|
clearTimeout(handler);
|
|
};
|
|
}, [value, delay]);
|
|
|
|
return debouncedValue;
|
|
}
|
|
|
|
/**
|
|
* Hook to track previous value
|
|
*/
|
|
export function usePrevious<T>(value: T): T | undefined {
|
|
const ref = useRef<T | undefined>(undefined);
|
|
|
|
useEffect(() => {
|
|
ref.current = value;
|
|
}, [value]);
|
|
|
|
return ref.current;
|
|
}
|
|
|
|
/**
|
|
* Hook for async operations with loading and error states
|
|
*/
|
|
interface UseAsyncOptions<T> {
|
|
onSuccess?: (data: T) => void;
|
|
onError?: (error: Error) => void;
|
|
}
|
|
|
|
export function useAsync<T, Args extends unknown[]>(
|
|
asyncFunction: (...args: Args) => Promise<T>,
|
|
options: UseAsyncOptions<T> = {}
|
|
) {
|
|
const [data, setData] = useState<T | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
|
|
const execute = useCallback(
|
|
async (...args: Args) => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const result = await asyncFunction(...args);
|
|
setData(result);
|
|
options.onSuccess?.(result);
|
|
return result;
|
|
} catch (err) {
|
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
setError(error);
|
|
options.onError?.(error);
|
|
throw error;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
},
|
|
[asyncFunction, options]
|
|
);
|
|
|
|
return { data, isLoading, error, execute, setData };
|
|
}
|
|
|
|
/**
|
|
* Hook for local storage with sync across tabs
|
|
*/
|
|
export function useLocalStorage<T>(
|
|
key: string,
|
|
initialValue: T
|
|
): [T, (value: T | ((val: T) => T)) => void] {
|
|
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
if (typeof window === "undefined") {
|
|
return initialValue;
|
|
}
|
|
try {
|
|
const item = window.localStorage.getItem(key);
|
|
return item ? (JSON.parse(item) as T) : initialValue;
|
|
} catch (error) {
|
|
console.error(`Error reading localStorage key "${key}":`, error);
|
|
return initialValue;
|
|
}
|
|
});
|
|
|
|
const setValue = useCallback(
|
|
(value: T | ((val: T) => T)) => {
|
|
try {
|
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
setStoredValue(valueToStore);
|
|
if (typeof window !== "undefined") {
|
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error setting localStorage key "${key}":`, error);
|
|
}
|
|
},
|
|
[key, storedValue]
|
|
);
|
|
|
|
// Listen for changes from other tabs
|
|
useEffect(() => {
|
|
const handleStorageChange = (event: StorageEvent) => {
|
|
if (event.key === key && event.newValue) {
|
|
try {
|
|
setStoredValue(JSON.parse(event.newValue) as T);
|
|
} catch (error) {
|
|
console.error(`Error parsing localStorage change for key "${key}":`, error);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener("storage", handleStorageChange);
|
|
return () => window.removeEventListener("storage", handleStorageChange);
|
|
}, [key]);
|
|
|
|
return [storedValue, setValue];
|
|
}
|
|
|
|
/**
|
|
* Hook to detect if user has reduced motion preference
|
|
*/
|
|
export function usePrefersReducedMotion(): boolean {
|
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
setPrefersReducedMotion(mediaQuery.matches);
|
|
|
|
const handler = (event: MediaQueryListEvent) => {
|
|
setPrefersReducedMotion(event.matches);
|
|
};
|
|
|
|
mediaQuery.addEventListener("change", handler);
|
|
return () => mediaQuery.removeEventListener("change", handler);
|
|
}, []);
|
|
|
|
return prefersReducedMotion;
|
|
}
|
|
|
|
/**
|
|
* Hook to track page visibility
|
|
*/
|
|
export function usePageVisibility(): boolean {
|
|
const [isVisible, setIsVisible] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const handleVisibilityChange = () => {
|
|
setIsVisible(!document.hidden);
|
|
};
|
|
|
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
}, []);
|
|
|
|
return isVisible;
|
|
}
|