# Frontend Stores Module ## Overview The Stores module implements **Redux Toolkit** state management for the frontend application. It provides a centralized store with core UI/auth slices, entity CRUD slices, and runtime-setting slices for transition and global UI-control defaults. **Location:** `frontend/src/stores/` **Total Files:** 26+ TypeScript files **Key Technologies:** - Redux Toolkit 2.1.0 - Axios for API calls - JWT for token handling --- ## Architecture ``` frontend/src/stores/ ├── store.ts # Main Redux store configuration (~49 LOC) ├── hooks.ts # Typed hooks (useAppDispatch, useAppSelector) (~6 LOC) ├── createEntitySlice.ts # Entity slice factory (~337 LOC) ├── selectors.ts # Memoized selectors (~181 LOC) │ ├── Core Slices (4) │ ├── authSlice.ts # Authentication state (~123 LOC) │ ├── styleSlice.ts # UI styling/theming (~107 LOC) │ ├── mainSlice.ts # Main app state (~32 LOC) │ ├── Entity/Runtime Setting Slices │ ├── users/usersSlice.ts # (~25 LOC) │ ├── roles/rolesSlice.ts # (~96 LOC) │ ├── permissions/permissionsSlice.ts # (~25 LOC) │ ├── projects/projectsSlice.ts # (~25 LOC) │ ├── project_memberships/project_membershipsSlice.ts # (~25 LOC) │ ├── assets/assetsSlice.ts # (~26 LOC) │ ├── asset_variants/asset_variantsSlice.ts # (~25 LOC) │ ├── presigned_url_requests/presigned_url_requestsSlice.ts # (~25 LOC) │ ├── tour_pages/tour_pagesSlice.ts # (~25 LOC) │ ├── project_audio_tracks/project_audio_tracksSlice.ts # (~25 LOC) │ ├── publish_events/publish_eventsSlice.ts # (~25 LOC) │ ├── pwa_caches/pwa_cachesSlice.ts # (~25 LOC) │ ├── access_logs/access_logsSlice.ts # (~25 LOC) │ ├── global_transition_defaults/globalTransitionDefaultsSlice.ts # Public GET (~138 LOC) │ ├── project_transition_settings/projectTransitionSettingsSlice.ts # Public prod GET (~275 LOC) │ ├── global_ui_control_defaults/globalUiControlDefaultsSlice.ts # Public GET singleton defaults │ ├── project_ui_control_settings/projectUiControlSettingsSlice.ts # Project/env UI-control overrides │ └── constructor/constructorSlice.ts # Constructor state (~110 LOC) │ ├── Data Files │ └── introSteps.ts # Onboarding tour definitions (~122 LOC) │ └── Legacy (deprecated) └── usersSlice.ts # Old users slice (use users/usersSlice.ts) ``` --- ## Store Configuration **File:** `store.ts` The main store combines all slice reducers: ```typescript import { configureStore } from '@reduxjs/toolkit'; export const store = configureStore({ reducer: { // Core slices style: styleReducer, main: mainReducer, auth: authSlice, // Entity and runtime-setting slices users: usersSlice, roles: rolesSlice, permissions: permissionsSlice, projects: projectsSlice, project_memberships: project_membershipsSlice, assets: assetsSlice, asset_variants: asset_variantsSlice, presigned_url_requests: presigned_url_requestsSlice, tour_pages: tour_pagesSlice, project_audio_tracks: project_audio_tracksSlice, publish_events: publish_eventsSlice, pwa_caches: pwa_cachesSlice, access_logs: access_logsSlice, global_transition_defaults: globalTransitionDefaultsSlice, project_transition_settings: projectTransitionSettingsSlice, global_ui_control_defaults: globalUiControlDefaultsSlice, project_ui_control_settings: projectUiControlSettingsSlice, }, }); // Inferred types for type safety export type RootState = ReturnType; export type AppDispatch = typeof store.dispatch; ``` --- ## Typed Hooks **File:** `hooks.ts` ```typescript import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import type { RootState, AppDispatch } from './store'; // Use throughout app instead of plain useDispatch and useSelector export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook = useSelector; ``` **Usage:** ```typescript import { useAppDispatch, useAppSelector } from '../stores/hooks'; const MyComponent = () => { const dispatch = useAppDispatch(); const users = useAppSelector((state) => state.users.users); const loading = useAppSelector((state) => state.users.loading); // Dispatch actions with full type inference dispatch(fetch({ query: '?limit=100' })); }; ``` --- ## Entity Slice Factory **File:** `createEntitySlice.ts` (~342 LOC) The factory generates Redux slices with standard CRUD operations for any entity. ### Configuration Interface ```typescript interface EntitySliceConfig { name: string; // Slice name (e.g., 'users') endpoint: string; // API endpoint (e.g., 'users') singularName?: string; // For notifications (e.g., 'User') } ``` ### Generated State ```typescript interface EntitySliceState { [entityName]: T[]; // Entity data array loading: boolean; // Loading indicator count: number; // Total count for pagination refetch: boolean; // Trigger refetch flag rolesWidgets: unknown[];// Widgets (for roles slice) notify: NotificationState; } interface NotificationState { showNotification: boolean; textNotification: string; typeNotification: 'warn' | 'error' | 'success' | 'info' | ''; } ``` ### Generated Async Thunks | Thunk | Action Type | Purpose | |-------|-------------|---------| | `fetch` | `{name}/fetch` | Fetch single by ID or list with query | | `create` | `{name}/create{Name}` | Create new entity | | `update` | `{name}/update{Name}` | Update existing entity | | `deleteItem` | `{name}/delete{Name}` | Delete single entity | | `deleteItemsByIds` | `{name}/deleteByIds` | Bulk delete by IDs | | `uploadCsv` | `{name}/uploadCsv` | Import CSV file | ### Generated Reducers | Reducer | Purpose | |---------|---------| | `setRefetch` | Trigger data refetch | ### Usage Example ```typescript // Creating a slice (entity file) import { createEntitySlice } from '../createEntitySlice'; import type { User } from '../../types/entities'; const { slice, actions, reducer } = createEntitySlice({ name: 'users', endpoint: 'users', singularName: 'User', }); export const { fetch, create, update, deleteItem, deleteItemsByIds, uploadCsv, setRefetch, } = actions; export default reducer; ``` ```typescript // Using in components import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { fetch, create, update, deleteItem } from '../stores/users/usersSlice'; const UsersPage = () => { const dispatch = useAppDispatch(); const { users, loading, count, notify } = useAppSelector((state) => state.users); // Fetch list with pagination useEffect(() => { dispatch(fetch({ query: '?page=1&limit=10' })); }, []); // Fetch single by ID const loadUser = (id: string) => { dispatch(fetch({ id })); }; // Create new const createUser = (data: Partial) => { dispatch(create(data)); }; // Update existing const updateUser = (id: string, data: Partial) => { dispatch(update({ id, data })); }; // Delete const removeUser = (id: string) => { dispatch(deleteItem(id)); }; }; ``` ### Factory Implementation Details ```typescript // Fetch thunk handles both single and list const fetch = createAsyncThunk< T | PaginatedResponse, FetchParams, { rejectValue: ApiError } >(`${name}/fetch`, async (data: FetchParams) => { const { id, query } = data; const url = `${endpoint}${query || (id ? `/${id}` : '')}`; const result = await axios.get>(url); if (id) { return result.data as T; // Single entity } return { rows: (result.data as PaginatedResponse).rows, count: (result.data as PaginatedResponse).count, }; }); // Extrareducers handle state updates builder.addCase(fetch.fulfilled, (state, action) => { const payload = action.payload; if (isPaginatedResponse(payload)) { state[name] = payload.rows; state.count = payload.count; } else { state[name] = payload; // Single entity stored in array } state.loading = false; }); ``` --- ## Core Slices ### 1. authSlice **File:** `authSlice.ts` (~130 LOC) **Purpose:** Authentication state management. ```typescript interface AuthState { isFetching: boolean; errorMessage: string; currentUser: User | null; token: string; notify: NotificationState; } ``` **Async Thunks:** | Thunk | Endpoint | Purpose | |-------|----------|---------| | `loginUser` | `POST /auth/signin/local` | Local authentication | | `findMe` | `GET /auth/me` | Fetch current user | | `passwordReset` | `PUT /auth/password-reset` | Reset password | **Reducers:** | Reducer | Purpose | |---------|---------| | `logoutUser` | Clear auth state, remove tokens | **Token Handling:** ```typescript builder.addCase(loginUser.fulfilled, (state, action) => { const token = action.payload; const user = jwt.decode(token); state.token = token; sessionStorage.setItem('token', token); localStorage.setItem('token', token); axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; }); ``` **Usage:** ```typescript import { loginUser, logoutUser, findMe } from '../stores/authSlice'; // Login dispatch(loginUser({ email, password })); // Logout dispatch(logoutUser()); // Fetch current user dispatch(findMe()); // Access state const { currentUser, isFetching, errorMessage } = useAppSelector(state => state.auth); ``` --- ### 2. styleSlice **File:** `styleSlice.ts` (~107 LOC) **Purpose:** UI theming and styling state. ```typescript interface StyleState { darkMode: boolean; bgLayoutColor: string; iconsColor: string; activeLinkColor: string; cardsColor: string; focusRingColor: string; corners: string; cardsStyle: string; linkColor: string; borders: string; shadow: string; textSecondary: string; // Aside menu styles asideStyle: string; asideScrollbarsStyle: string; asideBrandStyle: string; asideMenuItemStyle: string; asideMenuItemActiveStyle: string; asideMenuDropdownStyle: string; // NavBar styles navBarItemLabelStyle: string; navBarItemLabelHoverStyle: string; navBarItemLabelActiveColorStyle: string; overlayStyle: string; websiteHeder: string; websiteSectionStyle: string; } ``` **Reducers:** | Reducer | Purpose | |---------|---------| | `setDarkMode` | Toggle dark/light mode | | `setStyle` | Apply complete style theme | **Dark Mode Handling:** ```typescript setDarkMode: (state, action: PayloadAction) => { state.darkMode = action.payload !== null ? action.payload : !state.darkMode; localStorage.setItem(localStorageDarkModeKey, state.darkMode ? '1' : '0'); document.body.classList[state.darkMode ? 'add' : 'remove']('dark-scrollbars'); document.documentElement.classList[state.darkMode ? 'add' : 'remove']('dark-scrollbars-compat'); } ``` **Usage:** ```typescript import { setDarkMode, setStyle } from '../stores/styleSlice'; // Toggle dark mode dispatch(setDarkMode(null)); // Toggle dispatch(setDarkMode(true)); // Force dark dispatch(setDarkMode(false)); // Force light // Apply style theme dispatch(setStyle('white')); dispatch(setStyle('basic')); // Access state const { darkMode, corners, cardsStyle, iconsColor } = useAppSelector(state => state.style); ``` --- ### 3. mainSlice **File:** `mainSlice.ts` (~36 LOC) **Purpose:** Main application UI state. ```typescript interface MainState { userName: string; userEmail: string | null; userAvatar: string | null; isFieldFocusRegistered: boolean; } ``` **Reducers:** | Reducer | Purpose | |---------|---------| | `setUser` | Update user display info | **Usage:** ```typescript import { setUser } from '../stores/mainSlice'; dispatch(setUser({ name: 'John Doe', email: 'john@example.com', avatar: '/avatars/john.jpg', })); const { userName, userEmail, userAvatar } = useAppSelector(state => state.main); ``` --- ## Entity Slices All 13 entity slices follow the factory pattern: ### Standard Entity Slice Pattern ```typescript /** * {Entity} Redux Slice */ import { createEntitySlice } from '../createEntitySlice'; import type { EntityType } from '../../types/entities'; const { slice, actions, reducer } = createEntitySlice({ name: 'entity_name', endpoint: 'api_endpoint', singularName: 'Entity Name', }); export const { fetch, create, update, deleteItem, deleteItemsByIds, uploadCsv, setRefetch, } = actions; export const entitySlice = slice; export default reducer; ``` ### Entity Slice Inventory | Slice | Entity Type | Endpoint | Singular Name | |-------|-------------|----------|---------------| | `users` | `User` | `users` | User | | `roles` | `Role` | `roles` | Role | | `permissions` | `PermissionEntity` | `permissions` | Permission | | `projects` | `Project` | `projects` | Project | | `project_memberships` | `ProjectMembership` | `project_memberships` | Project Membership | | `assets` | `Asset` | `assets` | Asset | | `asset_variants` | `AssetVariant` | `asset_variants` | Asset Variant | | `presigned_url_requests` | `PresignedUrlRequest` | `presigned_url_requests` | Presigned URL Request | | `tour_pages` | `TourPage` | `tour_pages` | Tour Page | | `project_audio_tracks` | `ProjectAudioTrack` | `project_audio_tracks` | Project Audio Track | | `publish_events` | `PublishEvent` | `publish_events` | Publish Event | | `pwa_caches` | `PwaCache` | `pwa_caches` | PWA Cache | | `access_logs` | `AccessLog` | `access_logs` | Access Log | | `global_transition_defaults` | `GlobalTransitionDefaults` | `global-transition-defaults` | Global defaults (singleton, public GET) | | `project_transition_settings` | `ProjectTransitionSettingsEntity` | `project-transition-settings` | Per-project settings (public prod GET) | ## Type Definitions ### Redux Types (`types/redux.ts`) ```typescript // Notification state structure export interface NotificationState { showNotification: boolean; textNotification: string; typeNotification: 'warn' | 'error' | 'success' | 'info' | ''; } // Generic entity slice state export interface EntitySliceState { data: T[]; loading: boolean; count: number; refetch: boolean; notify: NotificationState; } // Slice factory configuration export interface EntitySliceConfig { name: string; endpoint: string; singularName?: string; } // Auth state structure export interface AuthState { currentUser: User | null; token: string | null; isFetching: boolean; errorMessage: string | null; notify: NotificationState; } ``` ### API Types (`types/api.ts`) ```typescript // Paginated response from API export interface PaginatedResponse { rows: T[]; count: number; } // Fetch parameters for entity queries export interface FetchParams { id?: string; query?: string; limit?: number; page?: number; } // API error response structure export interface ApiError { message?: string; statusCode?: number; errors?: Record; } // Sort model for data grid export interface SortModel { field: string; sort: 'asc' | 'desc'; } // Query parameters for list requests export interface ListQueryParams { page?: number; limit?: number; sort?: 'asc' | 'desc'; field?: string; [filterKey: string]: string | number | boolean | undefined; } ``` ### Entity Types (`types/entities.ts`) ```typescript // Base entity interface export interface BaseEntity { id: string; createdAt?: string; updatedAt?: string; } // User entity export interface User extends BaseEntity { firstName?: string; lastName?: string; email: string; disabled?: boolean; avatar?: ImageFile[]; app_role?: Role | null; custom_permissions?: PermissionEntity[]; } // Project entity export interface Project extends BaseEntity { name: string; slug?: string; description?: string; logo_url?: string; favicon_url?: string; og_image_url?: string; } // Asset entity export interface Asset extends BaseEntity { project?: Project | string | null; name: string; asset_type: 'image' | 'video' | 'audio' | 'file'; type?: 'general' | 'icon' | 'background_image' | 'audio' | 'video' | 'transition' | 'logo' | 'favicon' | 'document'; cdn_url?: string; storage_key?: string; mime_type?: string; size_mb?: number; width_px?: number; height_px?: number; duration_sec?: number; is_public?: boolean; is_deleted?: boolean; } // ... (13 total entity types) ``` --- ## Notification Helpers **File:** `helpers/notifyStateHandler.ts` Utility functions for managing notification state: ```typescript // Reset notification state export const resetNotify = (state) => { state.notify.showNotification = false; state.notify.typeNotification = ''; state.notify.textNotification = ''; }; // Handle rejected action with error notification export const rejectNotify = (state, action) => { if (typeof action.payload === 'string') { state.notify.textNotification = action.payload; } else if (typeof action === 'object') { // Parse validation errors const obj = { ...action.payload?.errors }; delete obj['_errors']; let msg = ''; for (const key in obj) { msg += `${key}: ${obj[key]['_errors']}; \n `; } state.notify.textNotification = msg; } else { state.notify.textNotification = 'Network error'; } state.notify.typeNotification = 'error'; state.notify.showNotification = true; }; // Handle fulfilled action with success notification export const fulfilledNotify = (state, msg) => { state.notify.textNotification = msg; state.notify.typeNotification = 'success'; state.notify.showNotification = true; }; ``` --- ## Onboarding Tour Steps **File:** `introSteps.ts` (~135 LOC) Defines step configurations for the app onboarding tour: ```typescript interface Step { element: string; // CSS selector intro: string; // HTML content position?: string; // Tooltip position tooltipClass?: string; // Custom CSS class disableInteraction?: boolean; } // Exported step arrays export const loginSteps: Step[] = [...]; // Login page tour export const appSteps: Step[] = [...]; // Main app tour export const usersSteps: Step[] = [...]; // Users page tour export const rolesSteps: Step[] = [...]; // Roles page tour ``` --- ## Data Flow Diagram ``` ┌─────────────────────────────────────────────────────────────────┐ │ Component │ │ useAppDispatch() + useAppSelector() │ └─────────────────────┬───────────────────────────────────────────┘ │ │ dispatch(fetch({ query })) ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Async Thunk │ │ fetch, create, update, deleteItem, etc. │ └─────────────────────┬───────────────────────────────────────────┘ │ │ axios.get/post/put/delete ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Backend API │ │ /api/{endpoint} │ └─────────────────────┬───────────────────────────────────────────┘ │ │ Response ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Extra Reducers │ │ pending → loading: true │ │ fulfilled → data: payload, loading: false │ │ rejected → notify: error, loading: false │ └─────────────────────┬───────────────────────────────────────────┘ │ │ State update ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Store │ │ RootState with 17 slices │ └─────────────────────┬───────────────────────────────────────────┘ │ │ useAppSelector subscription ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Component Re-render │ │ UI updates with new data │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Usage Patterns ### 1. List Page Pattern ```typescript import { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { fetch, setRefetch } from '../stores/users/usersSlice'; const UsersListPage = () => { const dispatch = useAppDispatch(); const { users, loading, count, refetch } = useAppSelector(state => state.users); const [page, setPage] = useState(1); // Initial fetch useEffect(() => { dispatch(fetch({ query: `?page=${page}&limit=10` })); }, [page, dispatch]); // Refetch trigger useEffect(() => { if (refetch) { dispatch(fetch({ query: `?page=${page}&limit=10` })); dispatch(setRefetch(false)); } }, [refetch, page, dispatch]); return ( ); }; ``` ### 2. Edit Page Pattern ```typescript import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { fetch, update } from '../stores/users/usersSlice'; const UserEditPage = () => { const router = useRouter(); const { id } = router.query; const dispatch = useAppDispatch(); const { users, loading, notify } = useAppSelector(state => state.users); // Fetch entity on mount useEffect(() => { if (id) { dispatch(fetch({ id: id as string })); } }, [id, dispatch]); // Get current entity const user = Array.isArray(users) ? users[0] : users; const handleSubmit = async (data: Partial) => { await dispatch(update({ id: id as string, data })); router.push('/users/users-list'); }; return ( {/* Form fields */} ); }; ``` ### 3. Create Page Pattern ```typescript import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { create } from '../stores/users/usersSlice'; const UserNewPage = () => { const dispatch = useAppDispatch(); const router = useRouter(); const { loading, notify } = useAppSelector(state => state.users); const handleSubmit = async (data: Partial) => { const result = await dispatch(create(data)); if (result.meta.requestStatus === 'fulfilled') { router.push('/users/users-list'); } }; return ( {/* Form fields */} ); }; ``` ### 4. Delete Pattern ```typescript import { deleteItem, deleteItemsByIds, setRefetch } from '../stores/users/usersSlice'; // Delete single const handleDelete = async (id: string) => { await dispatch(deleteItem(id)); dispatch(setRefetch(true)); }; // Bulk delete const handleBulkDelete = async (ids: string[]) => { await dispatch(deleteItemsByIds(ids)); dispatch(setRefetch(true)); }; ``` ### 5. Notification Handling ```typescript import { useEffect } from 'react'; import { toast } from 'react-toastify'; const MyComponent = () => { const { notify } = useAppSelector(state => state.users); useEffect(() => { if (notify.showNotification) { if (notify.typeNotification === 'success') { toast.success(notify.textNotification); } else if (notify.typeNotification === 'error') { toast.error(notify.textNotification); } } }, [notify]); }; ``` --- ## Store Provider Setup **File:** `_app.tsx` ```typescript import { Provider } from 'react-redux'; import { store } from '../stores/store'; function MyApp({ Component, pageProps }) { return ( ); } export default MyApp; ``` --- ## State Management Guidelines ### When to Use Redux (Default) Redux is the **default choice** for application state. Use Redux slices for: | State Type | Example | Why Redux | |------------|---------|-----------| | **Entity Data** | Users, Projects, Assets, Tour Pages | Shared across multiple components, API-backed | | **Authentication** | Current user, JWT token | App-wide, persisted to localStorage | | **UI Preferences** | Dark mode, theme settings | Persisted, affects entire app | | **Form State** | Complex multi-step forms | Survives navigation, shareable | | **Notifications** | Toast messages | Triggered from anywhere | ### When Local Hooks Are Acceptable Local React hooks (`useState`, custom hooks) are appropriate for: | State Type | Example | Why Local | |------------|---------|-----------| | **Ephemeral UI State** | Modal open/closed, dropdown expanded | Component-scoped, no persistence needed | | **Session-Scoped Navigation** | Page history in `usePageNavigation` | Isolated to single route, resets on unmount | | **Animation State** | Transition progress, fade states | Highly transient, no sharing needed | | **Form Field Focus** | Which input is focused | Local to form component | | **Derived/Computed Values** | Filtered lists, sorted data | Computed from Redux state | ### Decision Tree ``` Is the state needed by multiple unrelated components? ├── Yes → Use Redux └── No → Does the state need to persist across route changes? ├── Yes → Use Redux └── No → Is the state tied to API data? ├── Yes → Use Redux └── No → Local hook is acceptable ``` ### Example: usePageNavigation (Local Hook) The `usePageNavigation` hook uses local `useState` because: - ✅ Only used by RuntimePresentation OR constructor (never both simultaneously) - ✅ Session-scoped - history resets when leaving the tour - ✅ No cross-component sharing needed - ✅ No persistence requirement - ✅ Simple data structure (array + string) ```typescript // Appropriate for local hook const { currentPageId, pageHistory, applyPageSelection } = usePageNavigation({ pages, trackHistory: true, }); ``` ### Example: Constructor Elements (Redux) The `constructorSlice` uses Redux because: - ✅ Multiple components need access (canvas, sidebar, settings panels) - ✅ Complex nested state (elements, selections, undo history) - ✅ DevTools debugging valuable - ✅ Actions dispatched from many places ```typescript // Appropriate for Redux const elements = useAppSelector((state) => state.constructor.elements); dispatch(updateElement({ id, changes })); ``` ### Anti-Patterns to Avoid | Anti-Pattern | Problem | Solution | |--------------|---------|----------| | Redux for modal open/close | Over-engineering | Local `useState` | | Local state for entity data | Data inconsistency | Redux slice | | Prop drilling 3+ levels | Maintenance burden | Redux or Context | | Redux for animation frames | Performance issues | Local `useRef` | --- ## Summary | Category | Count | Description | |----------|-------|-------------| | Core Slices | 4 | auth, style, main, openAi | | Entity Slices | 15 | Generated via factory pattern (includes 2 transition slices) | | Constructor Slice | 1 | Tour editor state | | Type Files | 3 | redux.ts, entities.ts, api.ts | | Total Slices | 20 | Combined in store.ts | ### Public Access Slices Some slices support public API access for runtime presentations: | Slice | Public Access | Notes | |-------|--------------|-------| | `global_transition_defaults` | GET always public | No auth headers needed | | `project_transition_settings` | GET production only | Backend determines from URL path | These slices don't require special headers - the backend determines public access from the URL path itself. | Total Files | 22+ | In stores directory | **Key Patterns:** - **Redux is default** for app-wide, persistent, shared state - **Local hooks acceptable** for ephemeral, component-scoped state - Factory pattern eliminates ~2,600 LOC of boilerplate (13 entities × ~200 LOC each) - Typed hooks ensure type safety throughout - Consistent notification handling via helpers - Async thunks with proper error handling --- ## Related Documentation - [Frontend Architecture](./frontend-architecture.md) - Overall frontend structure - [Hooks Module](./hooks-module.md) - Custom React hooks - [Components Module](./components-module.md) - React components - [Backend API Endpoints](../../backend/docs/api-endpoints.md) - API reference