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>
95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
'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>
|
|
);
|
|
};
|