From 4323c4812c558ca96d65dd5deeaeef03f74d89b7 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 28 Jan 2026 04:29:06 +0530 Subject: [PATCH] Integrate Redux for state management and refactor authentication flow - Added Redux Toolkit for managing authentication state, replacing Zustand for token handling. - Implemented authSlice for token management, including setting and clearing tokens with localStorage synchronization. - Refactored components (Login, Home, TelegramLoginButton) to utilize Redux for token management, enhancing state consistency. - Updated App component to include Redux Provider and PersistGate for state persistence. - Removed Zustand-based authStore and socketStore, streamlining the state management approach. These changes improve the application's architecture by centralizing state management with Redux, enhancing maintainability and scalability. --- package.json | 5 + src/App.tsx | 25 ++- src/components/ConnectionIndicator.tsx | 4 +- src/components/TelegramLoginButton.tsx | 9 +- src/pages/Home.tsx | 7 +- src/pages/Login.tsx | 9 +- src/services/socketService.ts | 220 ------------------------- src/store/authSlice.ts | 38 +++++ src/store/authStore.ts | 81 --------- src/store/hooks.ts | 5 + src/store/index.ts | 122 ++++++++++++++ src/store/socketSlice.ts | 33 ++++ src/store/socketStore.ts | 28 ---- src/utils/desktopDeepLinkListener.ts | 5 +- yarn.lock | 84 ++++++++++ 15 files changed, 322 insertions(+), 353 deletions(-) delete mode 100644 src/services/socketService.ts create mode 100644 src/store/authSlice.ts delete mode 100644 src/store/authStore.ts create mode 100644 src/store/hooks.ts create mode 100644 src/store/index.ts create mode 100644 src/store/socketSlice.ts delete mode 100644 src/store/socketStore.ts diff --git a/package.json b/package.json index 365bac97e..1661639da 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "tauri": "tauri" }, "dependencies": { + "@reduxjs/toolkit": "^2.11.2", "@tauri-apps/api": "^2", "@tauri-apps/plugin-deep-link": "^2", "@tauri-apps/plugin-opener": "^2", @@ -17,7 +18,10 @@ "lottie-react": "^2.4.1", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-redux": "^9.2.0", "react-router-dom": "^7.13.0", + "redux-logger": "^3.0.6", + "redux-persist": "^6.0.0", "socket.io-client": "^4.8.3", "zustand": "^5.0.10" }, @@ -27,6 +31,7 @@ "@tauri-apps/cli": "^2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@types/redux-logger": "^3.0.13", "@vitejs/plugin-react": "^4.6.0", "autoprefixer": "^10.4.23", "postcss": "^8.5.6", diff --git a/src/App.tsx b/src/App.tsx index be9910c02..eb7fa532c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,7 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { PersistGate } from 'redux-persist/integration/react'; +import { store, persistor } from './store'; import Welcome from './pages/Welcome'; import Login from './pages/Login'; import Onboarding from './pages/onboarding/Onboarding'; @@ -6,15 +9,19 @@ import Home from './pages/Home'; function App() { return ( - - - } /> - } /> - } /> - } /> - } /> - - + + + + + } /> + } /> + } /> + } /> + } /> + + + + ); } diff --git a/src/components/ConnectionIndicator.tsx b/src/components/ConnectionIndicator.tsx index 7e7b63b81..3dafb6071 100644 --- a/src/components/ConnectionIndicator.tsx +++ b/src/components/ConnectionIndicator.tsx @@ -1,4 +1,4 @@ -import { useSocketStore } from '../store/socketStore'; +import { useAppSelector } from '../store/hooks'; interface ConnectionIndicatorProps { status?: 'connected' | 'disconnected' | 'connecting'; @@ -12,7 +12,7 @@ const ConnectionIndicator = ({ className = '', }: ConnectionIndicatorProps) => { // Use socket store status, but allow override via props - const storeStatus = useSocketStore((state) => state.status); + const storeStatus = useAppSelector((state) => state.socket.status); const status = overrideStatus || storeStatus; const statusConfig = { connected: { diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index 9807b5e8e..54aa648c6 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -1,7 +1,8 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { BACKEND_URL, TELEGRAM_BOT_ID } from '../utils/config'; -import { useAuthStore } from '../store/authStore'; +import { useAppDispatch } from '../store/hooks'; +import { setToken } from '../store/authSlice'; interface TelegramAuthData { id: number; @@ -27,8 +28,8 @@ const TelegramLoginButton = ({ disabled: externalDisabled = false, }: TelegramLoginButtonProps) => { const navigate = useNavigate(); + const dispatch = useAppDispatch(); const [isAuthenticating, setIsAuthenticating] = useState(false); - const setToken = useAuthStore((state) => state.setToken); const handleTelegramLogin = async () => { if (isAuthenticating || externalDisabled) return; @@ -80,7 +81,7 @@ const TelegramLoginButton = ({ const jwtToken = event.data.token; // Store JWT token in store (received from backend server) - setToken(jwtToken); + dispatch(setToken(jwtToken)); // Call onSuccess callback if provided, otherwise navigate to onboarding if (onSuccess) { @@ -181,7 +182,7 @@ const TelegramLoginButton = ({ } // Store JWT token in store (this is the JWT from the backend) - setToken(sessionToken); + dispatch(setToken(sessionToken)); // Store user data in localStorage for backward compatibility if (user) { diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 3388309b9..537cfd561 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -3,11 +3,12 @@ import { useNavigate } from 'react-router-dom'; import { openUrl } from '../utils/openUrl'; import { TELEGRAM_BOT_USERNAME } from '../utils/config'; import ConnectionIndicator from '../components/ConnectionIndicator'; -import { useAuthStore } from '../store/authStore'; +import { useAppDispatch } from '../store/hooks'; +import { clearToken } from '../store/authSlice'; const Home = () => { const navigate = useNavigate(); - const clearToken = useAuthStore((state) => state.clearToken); + const dispatch = useAppDispatch(); const [userName] = useState('Cyrus'); // Get current date @@ -47,7 +48,7 @@ const Home = () => { }; const handleLogout = () => { - clearToken(); + dispatch(clearToken()); navigate('/'); }; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 2c13bfcd7..a8419713b 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,11 +1,12 @@ import { useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { useAuthStore } from '../store/authStore'; +import { useAppDispatch } from '../store/hooks'; +import { setToken } from '../store/authSlice'; const Login = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const setToken = useAuthStore((state) => state.setToken); + const dispatch = useAppDispatch(); // Handle JWT token from URL query parameter (backend redirect) useEffect(() => { @@ -14,7 +15,7 @@ const Login = () => { if (token) { // Store the JWT token from the backend - setToken(token); + dispatch(setToken(token)); // Clear the token from URL for security const newSearchParams = new URLSearchParams(searchParams); @@ -28,7 +29,7 @@ const Login = () => { navigate('/onboarding/'); }, 100); } - }, [searchParams, setToken, navigate]); + }, [searchParams, dispatch, navigate]); diff --git a/src/services/socketService.ts b/src/services/socketService.ts deleted file mode 100644 index 6b7cf3a02..000000000 --- a/src/services/socketService.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { io, Socket } from 'socket.io-client'; -import { BACKEND_URL } from '../utils/config'; -import { useAuthStore } from '../store/authStore'; -import { useSocketStore } from '../store/socketStore'; - -class SocketService { - private socket: Socket | null = null; - private reconnectAttempts = 0; - private maxReconnectAttempts = 5; - private reconnectDelay = 1000; // Start with 1 second - - /** - * Connect to the Socket.IO server using the JWT token from the auth store - */ - connect(): void { - const token = useAuthStore.getState().token; - - if (!token) { - console.warn('[SocketService] No token available, cannot connect'); - useSocketStore.getState().setStatus('disconnected'); - return; - } - - // Disconnect existing connection if any - if (this.socket?.connected) { - this.disconnect(); - } - - console.log('[SocketService] Connecting to socket server...'); - useSocketStore.getState().setStatus('connecting'); - - this.socket = io(BACKEND_URL, { - auth: { - token, - }, - path: '/socket.io/', - transports: ['polling', 'websocket'], - reconnection: true, - reconnectionAttempts: this.maxReconnectAttempts, - reconnectionDelay: this.reconnectDelay, - reconnectionDelayMax: 5000, - }); - - this.setupEventHandlers(); - } - - /** - * Disconnect from the Socket.IO server - */ - disconnect(): void { - if (this.socket) { - console.log('[SocketService] Disconnecting from socket server...'); - this.socket.disconnect(); - this.socket = null; - this.reconnectAttempts = 0; - useSocketStore.getState().setStatus('disconnected'); - useSocketStore.getState().setSocketId(null); - } - } - - /** - * Check if the socket is connected - */ - isConnected(): boolean { - return this.socket?.connected ?? false; - } - - /** - * Get the socket instance (use with caution) - */ - getSocket(): Socket | null { - return this.socket; - } - - /** - * Emit an event to the server - */ - emit(event: string, data?: unknown): void { - if (!this.socket?.connected) { - console.warn(`[SocketService] Cannot emit '${event}': socket not connected`); - return; - } - - this.socket.emit(event, data); - } - - /** - * Listen to an event from the server - */ - on(event: string, callback: (...args: unknown[]) => void): void { - if (!this.socket) { - console.warn(`[SocketService] Cannot listen to '${event}': socket not initialized`); - return; - } - - this.socket.on(event, callback); - } - - /** - * Remove an event listener - */ - off(event: string, callback?: (...args: unknown[]) => void): void { - if (!this.socket) { - return; - } - - this.socket.off(event, callback); - } - - /** - * Listen to an event once - */ - once(event: string, callback: (...args: unknown[]) => void): void { - if (!this.socket) { - console.warn(`[SocketService] Cannot listen to '${event}': socket not initialized`); - return; - } - - this.socket.once(event, callback); - } - - /** - * Setup event handlers for connection, disconnection, and errors - */ - private setupEventHandlers(): void { - if (!this.socket) { - return; - } - - this.socket.on('connect', () => { - console.log('[SocketService] Connected to socket server:', this.socket?.id); - this.reconnectAttempts = 0; - useSocketStore.getState().setStatus('connected'); - useSocketStore.getState().setSocketId(this.socket?.id || null); - }); - - this.socket.on('disconnect', (reason) => { - console.log('[SocketService] Disconnected from socket server:', reason); - useSocketStore.getState().setStatus('disconnected'); - useSocketStore.getState().setSocketId(null); - - // If disconnected due to error, set status to connecting if we're trying to reconnect - if (reason === 'io server disconnect' || reason === 'io client disconnect') { - // Manual disconnect, keep as disconnected - } else { - // Network error or other, might be reconnecting - if (this.socket && !this.socket.disconnected) { - useSocketStore.getState().setStatus('connecting'); - } - } - }); - - this.socket.on('connect_error', (error) => { - console.error('[SocketService] Connection error:', error.message); - this.reconnectAttempts++; - useSocketStore.getState().setStatus('connecting'); - - // If max attempts reached, try reconnecting with fresh token - if (this.reconnectAttempts >= this.maxReconnectAttempts) { - console.log('[SocketService] Max reconnection attempts reached, reconnecting with fresh token...'); - useSocketStore.getState().setStatus('disconnected'); - setTimeout(() => { - this.connect(); - }, this.reconnectDelay * 2); - } - }); - - this.socket.on('ready', () => { - console.log('[SocketService] Server ready'); - useSocketStore.getState().setStatus('connected'); - }); - - this.socket.on('error', (error: { message?: string; status?: number; requestId?: string }) => { - console.error('[SocketService] Server error:', error); - // Don't change status on server errors, connection might still be active - }); - } -} - -// Export a singleton instance -export const socketService = new SocketService(); - -// Auto-connect when token is available -// Listen to auth store changes -if (typeof window !== 'undefined') { - let previousToken: string | null = null; - - const checkAndUpdateConnection = () => { - const currentToken = useAuthStore.getState().token; - - // Only update connection if token actually changed - if (currentToken !== previousToken) { - previousToken = currentToken; - - if (currentToken && !socketService.isConnected()) { - socketService.connect(); - } else if (!currentToken && socketService.isConnected()) { - socketService.disconnect(); - } - } - }; - - // Initial connection check - checkAndUpdateConnection(); - - // Subscribe to auth store changes - useAuthStore.subscribe((state) => { - const currentToken = state.token; - - if (currentToken !== previousToken) { - previousToken = currentToken; - - if (currentToken && !socketService.isConnected()) { - socketService.connect(); - } else if (!currentToken && socketService.isConnected()) { - socketService.disconnect(); - } - } - }); -} diff --git a/src/store/authSlice.ts b/src/store/authSlice.ts new file mode 100644 index 000000000..10b4e4bd5 --- /dev/null +++ b/src/store/authSlice.ts @@ -0,0 +1,38 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface AuthState { + token: string | null; +} + +const initialState: AuthState = { + token: null, +}; + +// Initialize from localStorage for backward compatibility +if (typeof window !== 'undefined') { + const legacyToken = localStorage.getItem('sessionToken'); + if (legacyToken) { + initialState.token = legacyToken; + } +} + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + setToken: (state, action: PayloadAction) => { + state.token = action.payload; + // Also sync to localStorage for backward compatibility + localStorage.setItem('sessionToken', action.payload); + }, + clearToken: (state) => { + state.token = null; + // Also clear from localStorage for backward compatibility + localStorage.removeItem('sessionToken'); + localStorage.removeItem('user'); + }, + }, +}); + +export const { setToken, clearToken } = authSlice.actions; +export default authSlice.reducer; diff --git a/src/store/authStore.ts b/src/store/authStore.ts deleted file mode 100644 index 2084cd5dd..000000000 --- a/src/store/authStore.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { create } from 'zustand'; -import { persist, createJSONStorage } from 'zustand/middleware'; - -interface AuthState { - token: string | null; - setToken: (token: string) => void; - clearToken: () => void; - isAuthenticated: () => boolean; -} - -const STORAGE_KEY = 'auth-storage'; - -// Custom storage that syncs with localStorage 'sessionToken' for backward compatibility -const customStorage = { - getItem: (name: string): string | null => { - // First check Zustand's persisted storage - const zustandData = localStorage.getItem(name); - if (zustandData) { - try { - const parsed = JSON.parse(zustandData); - if (parsed.state?.token) { - return zustandData; - } - } catch { - // Ignore parse errors - } - } - - // Fallback to legacy sessionToken for backward compatibility - const legacyToken = localStorage.getItem('sessionToken'); - if (legacyToken) { - return JSON.stringify({ state: { token: legacyToken }, version: 0 }); - } - - return null; - }, - setItem: (name: string, value: string): void => { - localStorage.setItem(name, value); - // Also sync to sessionToken for backward compatibility - try { - const parsed = JSON.parse(value); - if (parsed.state?.token) { - localStorage.setItem('sessionToken', parsed.state.token); - } - } catch { - // Ignore parse errors - } - }, - removeItem: (name: string): void => { - localStorage.removeItem(name); - localStorage.removeItem('sessionToken'); - }, -}; - -export const useAuthStore = create()( - persist( - (set, get) => ({ - token: null, - - setToken: (token: string) => { - set({ token }); - }, - - clearToken: () => { - set({ token: null }); - // Also clear user data - localStorage.removeItem('user'); - }, - - isAuthenticated: () => { - return get().token !== null; - }, - }), - { - name: STORAGE_KEY, - storage: createJSONStorage(() => customStorage), - // Only persist the token - partialize: (state) => ({ token: state.token }), - } - ) -); diff --git a/src/store/hooks.ts b/src/store/hooks.ts new file mode 100644 index 000000000..dc2a0c539 --- /dev/null +++ b/src/store/hooks.ts @@ -0,0 +1,5 @@ +import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; +import type { RootState, AppDispatch } from './index'; + +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 000000000..48752b7a9 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,122 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; +import authReducer from './authSlice'; +import socketReducer from './socketSlice'; + +// Persist config for auth only +const authPersistConfig = { + key: 'auth', + storage, + // Only persist the token + whitelist: ['token'], +}; + +// Custom storage that syncs with localStorage 'sessionToken' for backward compatibility +// redux-persist stores data as JSON string with the state object +const customStorage = { + getItem: (key: string): Promise => { + return new Promise((resolve) => { + // First check redux-persist storage (redux-persist adds 'persist:' prefix) + const persistKey = `persist:${key}`; + const persistData = localStorage.getItem(persistKey); + + if (persistData) { + try { + const parsed = JSON.parse(persistData); + // redux-persist format: { _persist: {...}, token: "..." } + if (parsed && typeof parsed === 'object') { + resolve(persistData); + return; + } + } catch { + // Ignore parse errors + } + } + + // Fallback to legacy sessionToken for backward compatibility + const legacyToken = localStorage.getItem('sessionToken'); + if (legacyToken) { + // Create redux-persist compatible format + const fallbackData = JSON.stringify({ + token: legacyToken, + _persist: { version: -1, rehydrated: true }, + }); + localStorage.setItem(persistKey, fallbackData); + resolve(fallbackData); + return; + } + + resolve(null); + }); + }, + setItem: (key: string, value: string): Promise => { + return new Promise((resolve) => { + const persistKey = `persist:${key}`; + localStorage.setItem(persistKey, value); + // Also sync to sessionToken for backward compatibility + try { + const parsed = JSON.parse(value); + if (parsed && typeof parsed === 'object' && parsed.token) { + localStorage.setItem('sessionToken', parsed.token); + } + } catch { + // Ignore parse errors + } + resolve(); + }); + }, + removeItem: (key: string): Promise => { + return new Promise((resolve) => { + const persistKey = `persist:${key}`; + localStorage.removeItem(persistKey); + localStorage.removeItem('sessionToken'); + resolve(); + }); + }, +}; + +const authPersistConfigWithCustomStorage = { + ...authPersistConfig, + storage: customStorage, +}; + +const persistedAuthReducer = persistReducer(authPersistConfigWithCustomStorage, authReducer); + +// Get logger only in dev mode +let loggerMiddleware: unknown = undefined; +if (import.meta.env.DEV) { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const createLogger = require('redux-logger'); + loggerMiddleware = createLogger.createLogger(); + } catch { + // Logger not available, continue without it + } +} + +export const store = configureStore({ + reducer: { + auth: persistedAuthReducer, + socket: socketReducer, + }, + middleware: (getDefaultMiddleware) => { + const middleware = getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + }); + + // Add redux-logger in development + if (loggerMiddleware) { + return middleware.concat(loggerMiddleware); + } + + return middleware; + }, +}); + +export const persistor = persistStore(store); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; diff --git a/src/store/socketSlice.ts b/src/store/socketSlice.ts new file mode 100644 index 000000000..d210b6328 --- /dev/null +++ b/src/store/socketSlice.ts @@ -0,0 +1,33 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export type SocketConnectionStatus = 'connected' | 'disconnected' | 'connecting'; + +interface SocketState { + status: SocketConnectionStatus; + socketId: string | null; +} + +const initialState: SocketState = { + status: 'disconnected', + socketId: null, +}; + +const socketSlice = createSlice({ + name: 'socket', + initialState, + reducers: { + setStatus: (state, action: PayloadAction) => { + state.status = action.payload; + }, + setSocketId: (state, action: PayloadAction) => { + state.socketId = action.payload; + }, + reset: (state) => { + state.status = 'disconnected'; + state.socketId = null; + }, + }, +}); + +export const { setStatus, setSocketId, reset } = socketSlice.actions; +export default socketSlice.reducer; diff --git a/src/store/socketStore.ts b/src/store/socketStore.ts deleted file mode 100644 index 6b6c09d85..000000000 --- a/src/store/socketStore.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { create } from 'zustand'; - -export type SocketConnectionStatus = 'connected' | 'disconnected' | 'connecting'; - -interface SocketState { - status: SocketConnectionStatus; - socketId: string | null; - setStatus: (status: SocketConnectionStatus) => void; - setSocketId: (socketId: string | null) => void; - reset: () => void; -} - -export const useSocketStore = create((set) => ({ - status: 'disconnected', - socketId: null, - - setStatus: (status: SocketConnectionStatus) => { - set({ status }); - }, - - setSocketId: (socketId: string | null) => { - set({ socketId }); - }, - - reset: () => { - set({ status: 'disconnected', socketId: null }); - }, -})); diff --git a/src/utils/desktopDeepLinkListener.ts b/src/utils/desktopDeepLinkListener.ts index 70d37993c..2c4311b07 100644 --- a/src/utils/desktopDeepLinkListener.ts +++ b/src/utils/desktopDeepLinkListener.ts @@ -1,7 +1,8 @@ import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link'; import { invoke } from '@tauri-apps/api/core'; import { BACKEND_URL } from './config'; -import { useAuthStore } from '../store/authStore'; +import { store } from '../store'; +import { setToken } from '../store/authSlice'; /** * Handle a list of deep link URLs delivered by the Tauri deep-link plugin. @@ -53,7 +54,7 @@ const handleDeepLinkUrls = async (urls: string[] | null | undefined) => { } // Store session token in store - useAuthStore.getState().setToken(sessionToken); + store.dispatch(setToken(sessionToken)); localStorage.setItem('deepLinkHandled', 'true'); if (user) { localStorage.setItem('user', JSON.stringify(user)); diff --git a/yarn.lock b/yarn.lock index 21ee595c6..644cf3ed6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -350,6 +350,18 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@reduxjs/toolkit@^2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz#582225acea567329ca6848583e7dd72580d38e82" + integrity sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ== + dependencies: + "@standard-schema/spec" "^1.0.0" + "@standard-schema/utils" "^0.3.0" + immer "^11.0.0" + redux "^5.0.1" + redux-thunk "^3.1.0" + reselect "^5.1.0" + "@rolldown/pluginutils@1.0.0-beta.27": version "1.0.0-beta.27" resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz" @@ -485,6 +497,16 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== +"@standard-schema/spec@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== + +"@standard-schema/utils@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b" + integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g== + "@tailwindcss/forms@^0.5.11": version "0.5.11" resolved "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz" @@ -662,6 +684,18 @@ dependencies: csstype "^3.2.2" +"@types/redux-logger@^3.0.13": + version "3.0.13" + resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.13.tgz#473e98428cdcc6dc93c908de66732bf932e36bc8" + integrity sha512-jylqZXQfMxahkuPcO8J12AKSSCQngdEWQrw7UiLUJzMBcv1r4Qg77P6mjGLjM27e5gFQDPD8vwUMJ9AyVxFSsg== + dependencies: + redux "^5.0.0" + +"@types/use-sync-external-store@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== + "@vitejs/plugin-react@^4.6.0": version "4.7.0" resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz" @@ -788,6 +822,11 @@ debug@^4.1.0, debug@^4.3.1, debug@~4.4.1: dependencies: ms "^2.1.3" +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + integrity sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" @@ -927,6 +966,11 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +immer@^11.0.0: + version "11.1.3" + resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.3.tgz#78681e1deb6cec39753acf04eb16d7576c04f4d6" + integrity sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q== + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" @@ -1166,6 +1210,14 @@ react-dom@^19.1.0: dependencies: scheduler "^0.27.0" +react-redux@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.2.0.tgz#96c3ab23fb9a3af2cb4654be4b51c989e32366f5" + integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g== + dependencies: + "@types/use-sync-external-store" "^0.0.6" + use-sync-external-store "^1.4.0" + react-refresh@^0.17.0: version "0.17.0" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz" @@ -1205,6 +1257,33 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redux-logger@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + integrity sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg== + dependencies: + deep-diff "^0.3.5" + +redux-persist@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" + integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== + +redux-thunk@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3" + integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== + +redux@^5.0.0, redux@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== + +reselect@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" + integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== + resolve@^1.1.7, resolve@^1.22.8: version "1.22.11" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz" @@ -1391,6 +1470,11 @@ update-browserslist-db@^1.2.0: escalade "^3.2.0" picocolors "^1.1.1" +use-sync-external-store@^1.4.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"