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"