ADD new frontend
This commit is contained in:
394
frontend/src/stores/ui.store.ts
Normal file
394
frontend/src/stores/ui.store.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
|
||||
export type Theme = 'light' | 'dark' | 'auto';
|
||||
export type Language = 'es' | 'en' | 'fr' | 'pt' | 'it';
|
||||
export type ViewMode = 'list' | 'grid' | 'card';
|
||||
export type SidebarState = 'expanded' | 'collapsed' | 'hidden';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: 'success' | 'error' | 'warning' | 'info';
|
||||
title: string;
|
||||
message?: string;
|
||||
duration?: number;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
persistent?: boolean;
|
||||
}
|
||||
|
||||
export interface Modal {
|
||||
id: string;
|
||||
type: 'dialog' | 'drawer' | 'fullscreen';
|
||||
title: string;
|
||||
content: React.ReactNode;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
closeable?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export interface UIState {
|
||||
// Theme & Appearance
|
||||
theme: Theme;
|
||||
language: Language;
|
||||
sidebarState: SidebarState;
|
||||
compactMode: boolean;
|
||||
reducedMotion: boolean;
|
||||
|
||||
// Layout & Navigation
|
||||
currentPage: string;
|
||||
breadcrumbs: Array<{ label: string; path: string }>;
|
||||
viewMode: ViewMode;
|
||||
|
||||
// Loading States
|
||||
globalLoading: boolean;
|
||||
loadingStates: Record<string, boolean>;
|
||||
|
||||
// Toasts & Notifications
|
||||
toasts: Toast[];
|
||||
|
||||
// Modals & Dialogs
|
||||
modals: Modal[];
|
||||
|
||||
// User Preferences
|
||||
preferences: {
|
||||
showTips: boolean;
|
||||
autoSave: boolean;
|
||||
confirmActions: boolean;
|
||||
defaultPageSize: number;
|
||||
dateFormat: string;
|
||||
numberFormat: string;
|
||||
timezone: string;
|
||||
};
|
||||
|
||||
// Actions
|
||||
setTheme: (theme: Theme) => void;
|
||||
setLanguage: (language: Language) => void;
|
||||
setSidebarState: (state: SidebarState) => void;
|
||||
setCompactMode: (compact: boolean) => void;
|
||||
setReducedMotion: (reduced: boolean) => void;
|
||||
|
||||
setCurrentPage: (page: string) => void;
|
||||
setBreadcrumbs: (breadcrumbs: Array<{ label: string; path: string }>) => void;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
|
||||
setGlobalLoading: (loading: boolean) => void;
|
||||
setLoading: (key: string, loading: boolean) => void;
|
||||
isLoading: (key: string) => boolean;
|
||||
|
||||
showToast: (toast: Omit<Toast, 'id'>) => string;
|
||||
hideToast: (id: string) => void;
|
||||
clearToasts: () => void;
|
||||
|
||||
showModal: (modal: Omit<Modal, 'id'>) => string;
|
||||
hideModal: (id: string) => void;
|
||||
clearModals: () => void;
|
||||
|
||||
updatePreference: <K extends keyof UIState['preferences']>(
|
||||
key: K,
|
||||
value: UIState['preferences'][K]
|
||||
) => void;
|
||||
resetPreferences: () => void;
|
||||
}
|
||||
|
||||
const defaultPreferences = {
|
||||
showTips: true,
|
||||
autoSave: true,
|
||||
confirmActions: true,
|
||||
defaultPageSize: 25,
|
||||
dateFormat: 'DD/MM/YYYY',
|
||||
numberFormat: 'european', // european, american
|
||||
timezone: 'Europe/Madrid',
|
||||
};
|
||||
|
||||
export const useUIStore = create<UIState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
theme: 'light',
|
||||
language: 'es',
|
||||
sidebarState: 'expanded',
|
||||
compactMode: false,
|
||||
reducedMotion: false,
|
||||
|
||||
currentPage: '',
|
||||
breadcrumbs: [],
|
||||
viewMode: 'list',
|
||||
|
||||
globalLoading: false,
|
||||
loadingStates: {},
|
||||
|
||||
toasts: [],
|
||||
modals: [],
|
||||
|
||||
preferences: defaultPreferences,
|
||||
|
||||
// Theme & Appearance actions
|
||||
setTheme: (theme: Theme) => {
|
||||
set({ theme });
|
||||
|
||||
// Apply theme to document
|
||||
const root = document.documentElement;
|
||||
if (theme === 'dark') {
|
||||
root.classList.add('dark');
|
||||
root.classList.remove('light');
|
||||
} else if (theme === 'light') {
|
||||
root.classList.add('light');
|
||||
root.classList.remove('dark');
|
||||
} else {
|
||||
// Auto theme - check system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (prefersDark) {
|
||||
root.classList.add('dark');
|
||||
root.classList.remove('light');
|
||||
} else {
|
||||
root.classList.add('light');
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setLanguage: (language: Language) => {
|
||||
set({ language });
|
||||
// You might want to trigger i18n language change here
|
||||
},
|
||||
|
||||
setSidebarState: (sidebarState: SidebarState) => {
|
||||
set({ sidebarState });
|
||||
},
|
||||
|
||||
setCompactMode: (compactMode: boolean) => {
|
||||
set({ compactMode });
|
||||
},
|
||||
|
||||
setReducedMotion: (reducedMotion: boolean) => {
|
||||
set({ reducedMotion });
|
||||
|
||||
// Apply reduced motion preference
|
||||
const root = document.documentElement;
|
||||
if (reducedMotion) {
|
||||
root.classList.add('reduce-motion');
|
||||
} else {
|
||||
root.classList.remove('reduce-motion');
|
||||
}
|
||||
},
|
||||
|
||||
// Navigation actions
|
||||
setCurrentPage: (currentPage: string) => {
|
||||
set({ currentPage });
|
||||
},
|
||||
|
||||
setBreadcrumbs: (breadcrumbs: Array<{ label: string; path: string }>) => {
|
||||
set({ breadcrumbs });
|
||||
},
|
||||
|
||||
setViewMode: (viewMode: ViewMode) => {
|
||||
set({ viewMode });
|
||||
},
|
||||
|
||||
// Loading actions
|
||||
setGlobalLoading: (globalLoading: boolean) => {
|
||||
set({ globalLoading });
|
||||
},
|
||||
|
||||
setLoading: (key: string, loading: boolean) => {
|
||||
set((state) => ({
|
||||
loadingStates: {
|
||||
...state.loadingStates,
|
||||
[key]: loading,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
isLoading: (key: string): boolean => {
|
||||
return get().loadingStates[key] ?? false;
|
||||
},
|
||||
|
||||
// Toast actions
|
||||
showToast: (toast: Omit<Toast, 'id'>): string => {
|
||||
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newToast: Toast = {
|
||||
...toast,
|
||||
id,
|
||||
duration: toast.duration ?? (toast.type === 'error' ? 0 : 5000), // Error toasts don't auto-dismiss
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
toasts: [...state.toasts, newToast],
|
||||
}));
|
||||
|
||||
// Auto-dismiss toast if duration is set
|
||||
if (newToast.duration && newToast.duration > 0) {
|
||||
setTimeout(() => {
|
||||
get().hideToast(id);
|
||||
}, newToast.duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
hideToast: (id: string) => {
|
||||
set((state) => ({
|
||||
toasts: state.toasts.filter(toast => toast.id !== id),
|
||||
}));
|
||||
},
|
||||
|
||||
clearToasts: () => {
|
||||
set({ toasts: [] });
|
||||
},
|
||||
|
||||
// Modal actions
|
||||
showModal: (modal: Omit<Modal, 'id'>): string => {
|
||||
const id = `modal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newModal: Modal = {
|
||||
...modal,
|
||||
id,
|
||||
size: modal.size ?? 'md',
|
||||
closeable: modal.closeable ?? true,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
modals: [...state.modals, newModal],
|
||||
}));
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
hideModal: (id: string) => {
|
||||
const { modals } = get();
|
||||
const modal = modals.find(m => m.id === id);
|
||||
|
||||
// Call onClose callback if provided
|
||||
if (modal?.onClose) {
|
||||
modal.onClose();
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
modals: state.modals.filter(modal => modal.id !== id),
|
||||
}));
|
||||
},
|
||||
|
||||
clearModals: () => {
|
||||
// Call onClose for all modals
|
||||
const { modals } = get();
|
||||
modals.forEach(modal => {
|
||||
if (modal.onClose) {
|
||||
modal.onClose();
|
||||
}
|
||||
});
|
||||
|
||||
set({ modals: [] });
|
||||
},
|
||||
|
||||
// Preferences actions
|
||||
updatePreference: <K extends keyof UIState['preferences']>(
|
||||
key: K,
|
||||
value: UIState['preferences'][K]
|
||||
) => {
|
||||
set((state) => ({
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
[key]: value,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
resetPreferences: () => {
|
||||
set({ preferences: defaultPreferences });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'ui-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({
|
||||
theme: state.theme,
|
||||
language: state.language,
|
||||
sidebarState: state.sidebarState,
|
||||
compactMode: state.compactMode,
|
||||
reducedMotion: state.reducedMotion,
|
||||
viewMode: state.viewMode,
|
||||
preferences: state.preferences,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Selectors for common use cases
|
||||
export const useLanguage = () => useUIStore((state) => state.language);
|
||||
export const useSidebar = () => useUIStore((state) => ({
|
||||
state: state.sidebarState,
|
||||
setState: state.setSidebarState,
|
||||
}));
|
||||
export const useCompactMode = () => useUIStore((state) => state.compactMode);
|
||||
export const useViewMode = () => useUIStore((state) => state.viewMode);
|
||||
|
||||
export const useLoading = (key?: string) => {
|
||||
if (key) {
|
||||
return useUIStore((state) => state.isLoading(key));
|
||||
}
|
||||
return useUIStore((state) => state.globalLoading);
|
||||
};
|
||||
|
||||
export const useToasts = () => useUIStore((state) => state.toasts);
|
||||
export const useModals = () => useUIStore((state) => state.modals);
|
||||
|
||||
export const useBreadcrumbs = () => useUIStore((state) => ({
|
||||
breadcrumbs: state.breadcrumbs,
|
||||
setBreadcrumbs: state.setBreadcrumbs,
|
||||
}));
|
||||
|
||||
export const usePreferences = () => useUIStore((state) => state.preferences);
|
||||
|
||||
// Hook for UI actions
|
||||
export const useUIActions = () => useUIStore((state) => ({
|
||||
setTheme: state.setTheme,
|
||||
setLanguage: state.setLanguage,
|
||||
setSidebarState: state.setSidebarState,
|
||||
setCompactMode: state.setCompactMode,
|
||||
setReducedMotion: state.setReducedMotion,
|
||||
setCurrentPage: state.setCurrentPage,
|
||||
setBreadcrumbs: state.setBreadcrumbs,
|
||||
setViewMode: state.setViewMode,
|
||||
setGlobalLoading: state.setGlobalLoading,
|
||||
setLoading: state.setLoading,
|
||||
showToast: state.showToast,
|
||||
hideToast: state.hideToast,
|
||||
clearToasts: state.clearToasts,
|
||||
showModal: state.showModal,
|
||||
hideModal: state.hideModal,
|
||||
clearModals: state.clearModals,
|
||||
updatePreference: state.updatePreference,
|
||||
resetPreferences: state.resetPreferences,
|
||||
}));
|
||||
|
||||
// Initialize theme on store creation
|
||||
if (typeof window !== 'undefined') {
|
||||
// Set initial theme based on stored preference or system preference
|
||||
const storedState = localStorage.getItem('ui-storage');
|
||||
if (storedState) {
|
||||
try {
|
||||
const { state } = JSON.parse(storedState);
|
||||
useUIStore.getState().setTheme(state.theme || 'auto');
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse stored UI state:', error);
|
||||
useUIStore.getState().setTheme('auto');
|
||||
}
|
||||
} else {
|
||||
useUIStore.getState().setTheme('auto');
|
||||
}
|
||||
|
||||
// Listen for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
const { theme } = useUIStore.getState();
|
||||
if (theme === 'auto') {
|
||||
useUIStore.getState().setTheme('auto');
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for reduced motion preference
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', (e) => {
|
||||
useUIStore.getState().setReducedMotion(e.matches);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user