Add missing pages with AppShell layout integration

- Created /track, /insights, /children, /family, /settings, /logout pages
- Wrapped all authenticated pages with AppShell and ProtectedRoute
- Updated AI assistant page to use AppShell layout
- All pages now have proper header/navigation and footer/tabbar
- Added responsive mobile and desktop layouts
- Integrated with existing navigation system

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
andupetcu
2025-09-30 22:05:56 +03:00
parent b48aaded05
commit b62342fe2d
10 changed files with 899 additions and 9 deletions

View File

@@ -0,0 +1,121 @@
import { useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
setOnlineStatus,
setSyncInProgress,
removePendingAction,
incrementRetryCount,
updateLastSyncTime,
} from '@/store/slices/offlineSlice';
import apiClient from '@/lib/api/client';
interface RootState {
offline: {
isOnline: boolean;
pendingActions: any[];
syncInProgress: boolean;
};
}
const MAX_RETRY_ATTEMPTS = 3;
export const useOfflineSync = () => {
const dispatch = useDispatch();
const { isOnline, pendingActions, syncInProgress } = useSelector(
(state: RootState) => state.offline
);
// Monitor online/offline status
useEffect(() => {
const handleOnline = () => {
dispatch(setOnlineStatus(true));
};
const handleOffline = () => {
dispatch(setOnlineStatus(false));
};
// Set initial status
dispatch(setOnlineStatus(navigator.onLine));
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, [dispatch]);
// Sync pending actions when online
const syncPendingActions = useCallback(async () => {
if (!isOnline || pendingActions.length === 0 || syncInProgress) {
return;
}
dispatch(setSyncInProgress(true));
for (const action of pendingActions) {
try {
// Attempt to replay the action
await replayAction(action);
// Remove from pending actions on success
dispatch(removePendingAction(action.id));
} catch (error) {
console.error(`Failed to sync action ${action.id}:`, error);
// Increment retry count
dispatch(incrementRetryCount(action.id));
// If max retries exceeded, remove the action
if (action.retryCount >= MAX_RETRY_ATTEMPTS) {
console.warn(`Max retries exceeded for action ${action.id}, removing from queue`);
dispatch(removePendingAction(action.id));
}
}
}
dispatch(setSyncInProgress(false));
dispatch(updateLastSyncTime());
}, [isOnline, pendingActions, syncInProgress, dispatch]);
// Trigger sync when coming online
useEffect(() => {
if (isOnline && pendingActions.length > 0) {
syncPendingActions();
}
}, [isOnline, pendingActions.length, syncPendingActions]);
// Replay a specific action
const replayAction = async (action: any) => {
const { type, payload } = action;
switch (type) {
case 'CREATE_ACTIVITY':
return await apiClient.post('/api/v1/activities', payload);
case 'UPDATE_ACTIVITY':
return await apiClient.put(`/api/v1/activities/${payload.id}`, payload);
case 'DELETE_ACTIVITY':
return await apiClient.delete(`/api/v1/activities/${payload.id}`);
case 'CREATE_CHILD':
return await apiClient.post('/api/v1/children', payload);
case 'UPDATE_CHILD':
return await apiClient.put(`/api/v1/children/${payload.id}`, payload);
default:
throw new Error(`Unknown action type: ${type}`);
}
};
return {
isOnline,
pendingActionsCount: pendingActions.length,
syncInProgress,
syncPendingActions,
};
};