ADD new frontend
This commit is contained in:
266
frontend/src/hooks/ui/useDebounce.ts
Normal file
266
frontend/src/hooks/ui/useDebounce.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Debounce hook for delaying function execution
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
interface DebounceOptions {
|
||||
leading?: boolean;
|
||||
trailing?: boolean;
|
||||
maxWait?: number;
|
||||
}
|
||||
|
||||
interface DebouncedState<T> {
|
||||
debouncedValue: T;
|
||||
isDebouncing: boolean;
|
||||
}
|
||||
|
||||
interface DebouncedActions {
|
||||
cancel: () => void;
|
||||
flush: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<DebounceOptions> = {
|
||||
leading: false,
|
||||
trailing: true,
|
||||
maxWait: Infinity,
|
||||
};
|
||||
|
||||
// Simple debounce hook for values
|
||||
export const useDebounce = <T>(
|
||||
value: T,
|
||||
delay: number,
|
||||
options: DebounceOptions = {}
|
||||
): DebouncedState<T> & DebouncedActions => {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
const [isDebouncing, setIsDebouncing] = useState(false);
|
||||
|
||||
const config = { ...DEFAULT_OPTIONS, ...options };
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
const maxWaitTimeoutRef = useRef<number | null>(null);
|
||||
const lastCallTimeRef = useRef<number>(0);
|
||||
const lastInvokeTimeRef = useRef<number>(0);
|
||||
const leadingRef = useRef<boolean>(false);
|
||||
|
||||
// Cancel pending debounce
|
||||
const cancel = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
if (maxWaitTimeoutRef.current) {
|
||||
window.clearTimeout(maxWaitTimeoutRef.current);
|
||||
maxWaitTimeoutRef.current = null;
|
||||
}
|
||||
setIsDebouncing(false);
|
||||
leadingRef.current = false;
|
||||
}, []);
|
||||
|
||||
// Flush (execute immediately)
|
||||
const flush = useCallback(() => {
|
||||
if (timeoutRef.current || maxWaitTimeoutRef.current) {
|
||||
setDebouncedValue(value);
|
||||
lastInvokeTimeRef.current = Date.now();
|
||||
setIsDebouncing(false);
|
||||
cancel();
|
||||
}
|
||||
}, [value, cancel]);
|
||||
|
||||
// Main debounce effect
|
||||
useEffect(() => {
|
||||
const now = Date.now();
|
||||
lastCallTimeRef.current = now;
|
||||
|
||||
// Leading edge
|
||||
if (config.leading && !leadingRef.current) {
|
||||
setDebouncedValue(value);
|
||||
lastInvokeTimeRef.current = now;
|
||||
leadingRef.current = true;
|
||||
|
||||
if (!config.trailing) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsDebouncing(true);
|
||||
|
||||
// Clear existing timeouts
|
||||
if (timeoutRef.current) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
}
|
||||
if (maxWaitTimeoutRef.current) {
|
||||
window.clearTimeout(maxWaitTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Set up trailing timeout
|
||||
if (config.trailing) {
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
if (lastCallTimeRef.current === now) {
|
||||
setDebouncedValue(value);
|
||||
lastInvokeTimeRef.current = Date.now();
|
||||
setIsDebouncing(false);
|
||||
leadingRef.current = false;
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// Set up max wait timeout
|
||||
if (config.maxWait !== Infinity) {
|
||||
const timeSinceLastInvoke = now - lastInvokeTimeRef.current;
|
||||
const remainingWait = config.maxWait - timeSinceLastInvoke;
|
||||
|
||||
if (remainingWait <= 0) {
|
||||
setDebouncedValue(value);
|
||||
lastInvokeTimeRef.current = now;
|
||||
setIsDebouncing(false);
|
||||
leadingRef.current = false;
|
||||
} else {
|
||||
maxWaitTimeoutRef.current = window.setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
lastInvokeTimeRef.current = Date.now();
|
||||
setIsDebouncing(false);
|
||||
leadingRef.current = false;
|
||||
}, remainingWait);
|
||||
}
|
||||
}
|
||||
|
||||
return cancel;
|
||||
}, [value, delay, config.leading, config.trailing, config.maxWait, cancel]);
|
||||
|
||||
return {
|
||||
debouncedValue,
|
||||
isDebouncing,
|
||||
cancel,
|
||||
flush,
|
||||
};
|
||||
};
|
||||
|
||||
// Advanced debounce hook for functions
|
||||
export const useDebouncedCallback = <T extends (...args: any[]) => any>(
|
||||
callback: T,
|
||||
delay: number,
|
||||
options: DebounceOptions = {}
|
||||
): {
|
||||
debouncedCallback: T;
|
||||
isDebouncing: boolean;
|
||||
cancel: () => void;
|
||||
flush: () => void;
|
||||
} => {
|
||||
const [isDebouncing, setIsDebouncing] = useState(false);
|
||||
|
||||
const config = { ...DEFAULT_OPTIONS, ...options };
|
||||
const callbackRef = useRef(callback);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
const maxWaitTimeoutRef = useRef<number | null>(null);
|
||||
const argsRef = useRef<Parameters<T> | null>(null);
|
||||
const lastCallTimeRef = useRef<number>(0);
|
||||
const lastInvokeTimeRef = useRef<number>(0);
|
||||
const leadingRef = useRef<boolean>(false);
|
||||
|
||||
// Update callback ref
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Cancel pending execution
|
||||
const cancel = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
if (maxWaitTimeoutRef.current) {
|
||||
window.clearTimeout(maxWaitTimeoutRef.current);
|
||||
maxWaitTimeoutRef.current = null;
|
||||
}
|
||||
setIsDebouncing(false);
|
||||
leadingRef.current = false;
|
||||
argsRef.current = null;
|
||||
}, []);
|
||||
|
||||
// Flush (execute immediately)
|
||||
const flush = useCallback(() => {
|
||||
if (argsRef.current && (timeoutRef.current || maxWaitTimeoutRef.current)) {
|
||||
const result = callbackRef.current(...argsRef.current);
|
||||
lastInvokeTimeRef.current = Date.now();
|
||||
setIsDebouncing(false);
|
||||
cancel();
|
||||
return result;
|
||||
}
|
||||
}, [cancel]);
|
||||
|
||||
// Execute function
|
||||
const invokeFunc = useCallback((args: Parameters<T>) => {
|
||||
const result = callbackRef.current(...args);
|
||||
lastInvokeTimeRef.current = Date.now();
|
||||
setIsDebouncing(false);
|
||||
leadingRef.current = false;
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
// Debounced callback
|
||||
const debouncedCallback = useCallback(
|
||||
((...args: Parameters<T>) => {
|
||||
const now = Date.now();
|
||||
argsRef.current = args;
|
||||
lastCallTimeRef.current = now;
|
||||
|
||||
// Leading edge
|
||||
if (config.leading && !leadingRef.current) {
|
||||
const result = invokeFunc(args);
|
||||
leadingRef.current = true;
|
||||
|
||||
if (!config.trailing) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
setIsDebouncing(true);
|
||||
|
||||
// Clear existing timeouts
|
||||
if (timeoutRef.current) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
}
|
||||
if (maxWaitTimeoutRef.current) {
|
||||
window.clearTimeout(maxWaitTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Set up trailing timeout
|
||||
if (config.trailing) {
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
if (argsRef.current && lastCallTimeRef.current === now) {
|
||||
invokeFunc(argsRef.current);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// Set up max wait timeout
|
||||
if (config.maxWait !== Infinity) {
|
||||
const timeSinceLastInvoke = now - lastInvokeTimeRef.current;
|
||||
const remainingWait = config.maxWait - timeSinceLastInvoke;
|
||||
|
||||
if (remainingWait <= 0) {
|
||||
return invokeFunc(args);
|
||||
} else {
|
||||
maxWaitTimeoutRef.current = window.setTimeout(() => {
|
||||
if (argsRef.current) {
|
||||
invokeFunc(argsRef.current);
|
||||
}
|
||||
}, remainingWait);
|
||||
}
|
||||
}
|
||||
}) as T,
|
||||
[delay, config.leading, config.trailing, config.maxWait, invokeFunc]
|
||||
);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return cancel;
|
||||
}, [cancel]);
|
||||
|
||||
return {
|
||||
debouncedCallback,
|
||||
isDebouncing,
|
||||
cancel,
|
||||
flush,
|
||||
};
|
||||
};
|
||||
114
frontend/src/hooks/ui/useModal.ts
Normal file
114
frontend/src/hooks/ui/useModal.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Modal hook for managing modal state and behavior
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
interface ModalState {
|
||||
isOpen: boolean;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
interface ModalOptions {
|
||||
closeOnEscape?: boolean;
|
||||
closeOnBackdropClick?: boolean;
|
||||
preventBodyScroll?: boolean;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
interface ModalActions {
|
||||
openModal: (data?: any) => void;
|
||||
closeModal: () => void;
|
||||
toggleModal: () => void;
|
||||
setData: (data: any) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<ModalOptions> = {
|
||||
closeOnEscape: true,
|
||||
closeOnBackdropClick: true,
|
||||
preventBodyScroll: true,
|
||||
autoFocus: true,
|
||||
};
|
||||
|
||||
export const useModal = (
|
||||
initialState: boolean = false,
|
||||
options: ModalOptions = {}
|
||||
): ModalState & ModalActions => {
|
||||
const [state, setState] = useState<ModalState>({
|
||||
isOpen: initialState,
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const config = { ...DEFAULT_OPTIONS, ...options };
|
||||
|
||||
// Open modal
|
||||
const openModal = useCallback((data?: any) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isOpen: true,
|
||||
data,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Close modal
|
||||
const closeModal = useCallback(() => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isOpen: false,
|
||||
data: undefined,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Toggle modal
|
||||
const toggleModal = useCallback(() => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isOpen: !prev.isOpen,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Set modal data
|
||||
const setData = useCallback((data: any) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
data,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Handle escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (config.closeOnEscape && event.key === 'Escape' && state.isOpen) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
if (state.isOpen) {
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [state.isOpen, config.closeOnEscape, closeModal]);
|
||||
|
||||
// Handle body scroll prevention
|
||||
useEffect(() => {
|
||||
if (config.preventBodyScroll && state.isOpen) {
|
||||
const originalOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = originalOverflow;
|
||||
};
|
||||
}
|
||||
}, [state.isOpen, config.preventBodyScroll]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
openModal,
|
||||
closeModal,
|
||||
toggleModal,
|
||||
setData,
|
||||
};
|
||||
};
|
||||
182
frontend/src/hooks/ui/useToast.ts
Normal file
182
frontend/src/hooks/ui/useToast.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Toast hook for managing toast notifications
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
export type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
title?: string;
|
||||
message: string;
|
||||
duration?: number;
|
||||
dismissible?: boolean;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
toasts: Toast[];
|
||||
position: ToastPosition;
|
||||
maxToasts: number;
|
||||
}
|
||||
|
||||
interface ToastOptions {
|
||||
type?: ToastType;
|
||||
title?: string;
|
||||
duration?: number;
|
||||
dismissible?: boolean;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface ToastActions {
|
||||
addToast: (message: string, options?: ToastOptions) => string;
|
||||
removeToast: (id: string) => void;
|
||||
clearToasts: () => void;
|
||||
success: (message: string, options?: Omit<ToastOptions, 'type'>) => string;
|
||||
error: (message: string, options?: Omit<ToastOptions, 'type'>) => string;
|
||||
warning: (message: string, options?: Omit<ToastOptions, 'type'>) => string;
|
||||
info: (message: string, options?: Omit<ToastOptions, 'type'>) => string;
|
||||
setPosition: (position: ToastPosition) => void;
|
||||
setMaxToasts: (max: number) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_DURATION = 5000; // 5 seconds
|
||||
const DEFAULT_POSITION: ToastPosition = 'top-right';
|
||||
const DEFAULT_MAX_TOASTS = 6;
|
||||
|
||||
// Generate unique ID
|
||||
const generateId = (): string => {
|
||||
return `toast_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
};
|
||||
|
||||
export const useToast = (
|
||||
initialPosition: ToastPosition = DEFAULT_POSITION,
|
||||
initialMaxToasts: number = DEFAULT_MAX_TOASTS
|
||||
): ToastState & ToastActions => {
|
||||
const [state, setState] = useState<ToastState>({
|
||||
toasts: [],
|
||||
position: initialPosition,
|
||||
maxToasts: initialMaxToasts,
|
||||
});
|
||||
|
||||
// Remove toast by ID
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
toasts: prev.toasts.filter(toast => toast.id !== id),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Add toast
|
||||
const addToast = useCallback((message: string, options: ToastOptions = {}): string => {
|
||||
const id = generateId();
|
||||
|
||||
const toast: Toast = {
|
||||
id,
|
||||
type: options.type || 'info',
|
||||
title: options.title,
|
||||
message,
|
||||
duration: options.duration ?? DEFAULT_DURATION,
|
||||
dismissible: options.dismissible ?? true,
|
||||
action: options.action,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
setState(prev => {
|
||||
const newToasts = [...prev.toasts, toast];
|
||||
|
||||
// Limit number of toasts
|
||||
if (newToasts.length > prev.maxToasts) {
|
||||
return {
|
||||
...prev,
|
||||
toasts: newToasts.slice(-prev.maxToasts),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
toasts: newToasts,
|
||||
};
|
||||
});
|
||||
|
||||
// Auto-dismiss toast if duration is set
|
||||
if (toast.duration && toast.duration > 0) {
|
||||
setTimeout(() => {
|
||||
removeToast(id);
|
||||
}, toast.duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
}, [removeToast]);
|
||||
|
||||
// Clear all toasts
|
||||
const clearToasts = useCallback(() => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
toasts: [],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Convenience methods for different toast types
|
||||
const success = useCallback((message: string, options: Omit<ToastOptions, 'type'> = {}) => {
|
||||
return addToast(message, { ...options, type: 'success' });
|
||||
}, [addToast]);
|
||||
|
||||
const error = useCallback((message: string, options: Omit<ToastOptions, 'type'> = {}) => {
|
||||
return addToast(message, { ...options, type: 'error', duration: options.duration ?? 8000 });
|
||||
}, [addToast]);
|
||||
|
||||
const warning = useCallback((message: string, options: Omit<ToastOptions, 'type'> = {}) => {
|
||||
return addToast(message, { ...options, type: 'warning' });
|
||||
}, [addToast]);
|
||||
|
||||
const info = useCallback((message: string, options: Omit<ToastOptions, 'type'> = {}) => {
|
||||
return addToast(message, { ...options, type: 'info' });
|
||||
}, [addToast]);
|
||||
|
||||
// Set toast position
|
||||
const setPosition = useCallback((position: ToastPosition) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
position,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Set maximum number of toasts
|
||||
const setMaxToasts = useCallback((maxToasts: number) => {
|
||||
setState(prev => {
|
||||
const newToasts = prev.toasts.length > maxToasts
|
||||
? prev.toasts.slice(-maxToasts)
|
||||
: prev.toasts;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
maxToasts,
|
||||
toasts: newToasts,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
addToast,
|
||||
removeToast,
|
||||
clearToasts,
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
setPosition,
|
||||
setMaxToasts,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user