ADD new frontend

This commit is contained in:
Urtzi Alfaro
2025-08-28 10:41:04 +02:00
parent 9c247a5f99
commit 0fd273cfce
492 changed files with 114979 additions and 1632 deletions

View 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,
};
};

View 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,
};
};

View 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,
};
};