feat: Implement offline-first Redux architecture with optimistic updates
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

Implemented comprehensive offline-first state management:

Redux Store Setup:
- Configure Redux Toolkit with @redux-offline/redux-offline
- Setup redux-persist with IndexedDB (localforage)
- Custom offline config with exponential backoff retry
- Normalized state with entity adapters

State Slices:
- activitiesSlice: Normalized activities with optimistic CRUD
- childrenSlice: Normalized children with optimistic CRUD
- networkSlice: Network status and connection quality
- offlineSlice: Sync queue and pending actions

Middleware:
- offlineMiddleware: Queue actions when offline
- syncMiddleware: Process pending actions when online
- Conflict resolution strategies (SERVER_WINS, CLIENT_WINS, LAST_WRITE_WINS, MERGE)
- Version-based conflict detection

Features:
- Optimistic updates for immediate UI feedback
- Automatic sync queue with retry logic (5 retries max)
- Network detection (browser events + periodic checks)
- Connection quality monitoring (excellent/good/poor/offline)
- Latency tracking
- Conflict resolution with multiple strategies
- Entity versioning for optimistic updates

Components:
- NetworkStatusIndicator: Full-screen status banner
- NetworkStatusBadge: Compact app bar badge
- ReduxProvider: Provider with network detection setup

Custom Hooks:
- useAppDispatch/useAppSelector: Typed Redux hooks
- useIsOnline: Check online status
- useHasPendingSync: Check for pending actions
- useSyncStatus: Get sync progress info
- useOptimisticAction: Combine optimistic + actual actions
- useNetworkQuality: Get connection quality
- useIsOptimistic: Check if entity is being synced

Documentation:
- Comprehensive README with usage examples
- Architecture overview
- Best practices guide
- Troubleshooting section

State Structure:
- Normalized entities with byId/allIds
- Optimistic metadata (_optimistic, _localId, _version)
- Entity adapters with memoized selectors
- Offline queue persistence to IndexedDB

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-01 19:24:46 +00:00
parent aaa239121e
commit 7cb2ff97de
12 changed files with 1469 additions and 563 deletions

View File

@@ -0,0 +1,94 @@
'use client';
import { Alert, Snackbar, Box, Typography, CircularProgress } from '@mui/material';
import { CloudOff, CloudQueue, CloudDone, Wifi, WifiOff } from '@mui/icons-material';
import { useIsOnline, useSyncStatus } from '@/store/hooks';
export const NetworkStatusIndicator: React.FC = () => {
const isOnline = useIsOnline();
const { syncing, pendingCount } = useSyncStatus();
// Show nothing if online and no pending sync
if (isOnline && !syncing && pendingCount === 0) {
return null;
}
return (
<Snackbar
open={true}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
sx={{ top: 80 }}
>
<Alert
severity={isOnline ? (syncing ? 'info' : 'warning') : 'error'}
icon={
syncing ? (
<CircularProgress size={20} />
) : isOnline ? (
<CloudQueue />
) : (
<WifiOff />
)
}
sx={{
width: '100%',
maxWidth: 400,
borderRadius: 2,
}}
>
<Box>
<Typography variant="body2" fontWeight="600">
{!isOnline && 'You are offline'}
{isOnline && syncing && 'Syncing changes...'}
{isOnline && !syncing && pendingCount > 0 && `${pendingCount} changes pending`}
</Typography>
<Typography variant="caption">
{!isOnline && 'Your changes will be saved and synced when you reconnect'}
{isOnline && syncing && 'Please wait while we sync your data'}
{isOnline && !syncing && pendingCount > 0 && 'Waiting to sync'}
</Typography>
</Box>
</Alert>
</Snackbar>
);
};
/**
* Small status badge for the app bar
*/
export const NetworkStatusBadge: React.FC = () => {
const isOnline = useIsOnline();
const { syncing, pendingCount } = useSyncStatus();
if (isOnline && !syncing && pendingCount === 0) {
return null;
}
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
px: 1.5,
py: 0.5,
borderRadius: 2,
bgcolor: isOnline ? 'warning.light' : 'error.light',
color: isOnline ? 'warning.dark' : 'error.dark',
}}
>
{syncing ? (
<CircularProgress size={14} />
) : isOnline ? (
<CloudQueue sx={{ fontSize: 16 }} />
) : (
<WifiOff sx={{ fontSize: 16 }} />
)}
<Typography variant="caption" fontWeight="600">
{!isOnline && 'Offline'}
{isOnline && syncing && 'Syncing'}
{isOnline && !syncing && pendingCount > 0 && pendingCount}
</Typography>
</Box>
);
};

View File

@@ -0,0 +1,24 @@
'use client';
import { useEffect, useRef } from 'react';
import { Provider } from 'react-redux';
import { store } from '@/store/store';
import { setupNetworkDetection } from '@/store/middleware/offlineMiddleware';
export function ReduxProvider({ children }: { children: React.Node }) {
const cleanupRef = useRef<(() => void) | null>(null);
useEffect(() => {
// Setup network detection
cleanupRef.current = setupNetworkDetection(store.dispatch);
// Cleanup on unmount
return () => {
if (cleanupRef.current) {
cleanupRef.current();
}
};
}, []);
return <Provider store={store}>{children}</Provider>;
}