feat: Implement offline-first Redux architecture with optimistic updates
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:
94
maternal-web/components/common/NetworkStatusIndicator.tsx
Normal file
94
maternal-web/components/common/NetworkStatusIndicator.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
24
maternal-web/components/providers/ReduxProvider.tsx
Normal file
24
maternal-web/components/providers/ReduxProvider.tsx
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user