EduMitra is a comprehensive mobile-first education and career companion app built during our first hackathon. This application addresses the problem of students managing academics, exams, and careers across multiple disconnected apps by providing a unified, personalized platform with offline functionality and smart features.
Challenge: Students manage academics, exams, and careers across multiple apps, often struggling with connectivity and lack of personalization.
Solution: Build a mobile-first education & career companion app with offline functionality, personalized dashboards, and smart nudges to guide students in academics and careers.
- Secure user registration and login
- JWT-based authentication
- Form validation with error handling
- Professional UI with modern design
- Password encryption using bcryptjs
- Browse and enroll in courses
- Course progress tracking
- Personal course dashboard
- Course recommendations powered by AI
- Real-time enrollment status
- Create and manage learning goals
- Progress tracking with visual indicators
- Milestone management
- Priority-based goal organization
- Goal statistics and analytics
- Course-specific note taking
- Organized note management
- Search and filter capabilities
- Sync across devices
- Role-specific interview questions
- Real-time answer submission
- AI evaluation using Ollama & Mistral
- Detailed feedback with scores
- Interview history and analytics
- Performance tracking
- AI-powered resume generation
- Multiple professional templates (Modern, Classic, Creative, Minimal)
- Personal information management
- Education and experience tracking
- PDF generation and download
- Resume analytics
- Company-specific skill requirements
- Skill gap analysis
- Personalized learning paths
- Career advancement recommendations
- Progress tracking
- Peer group creation and management
- Group chat functionality
- Study groups and collaboration
- Mentorship connections
- Personalized student dashboard
- Parent/Guardian dashboard
- Recent activity tracking
- Progress visualization
- Performance insights
- Attendance tracking
- Exam preparation resources
- Academic performance monitoring
- Score tracking
- Internship opportunities
- Scholarship information
- Application tracking
- Career guidance
Technology Stack:
- React Native 0.79.5
- Expo Router for navigation
- TypeScript for type safety
- Zustand for state management
- React Hook Form for validation
- AsyncStorage for offline data
- Axios for API communication
Technology Stack:
- Node.js with Express.js
- MongoDB with Mongoose ODM
- JWT authentication
- bcryptjs for password hashing
- CORS for cross-origin requests
- Ollama + Mistral for AI features
- Puppeteer for PDF generation
AI-Powered Features:
- Ollama with Mistral model for:
✓ Course recommendations
✓ Resume content generation
✓ Mock interview evaluation
✓ Skill gap analysis
✓ Career guidance
frontend/
├── app/ # Expo Router app directory
│ ├── _layout.tsx # Root layout with navigation setup
│ ├── index.tsx # Entry point - redirects to auth
│ ├── (auth)/ # Authentication group
│ │ ├── _layout.tsx # Auth layout (stack navigation)
│ │ ├── signin.tsx # Sign In screen
│ │ └── signup.tsx # Sign Up screen
│ ├── (tabs)/ # Main app tabs (after authentication)
│ │ ├── _layout.tsx # Tab navigation layout
│ │ ├── index.tsx # Home dashboard
│ │ ├── ndk.tsx # Learning/courses tab
│ │ └── explore.tsx # Discovery tab
│ ├── (main)/ # Feature screens (30+ screens)
│ │ ├── dashboard.tsx # Personal dashboard
│ │ ├── courses.tsx # Course management
│ │ ├── goals.tsx # Goal tracking
│ │ ├── notes.tsx # Note taking
│ │ ├── mock-tests.tsx # Interview practice
│ │ ├── resume.tsx # Resume builder
│ │ ├── skills.tsx # Skill mapping
│ │ ├── peer-groups.tsx # Community
│ │ ├── academics.tsx # Academic tracking
│ │ ├── exam-prep.tsx # Exam preparation
│ │ ├── internships.tsx # Internship portal
│ │ ├── scholarships.tsx # Scholarship info
│ │ └── parent-dashboard.tsx # Parent view
│ └── +not-found.tsx # 404 screen
├── components/
│ ├── common/ # Reusable UI components
│ │ ├── Input.tsx # Custom input with validation
│ │ ├── Button.tsx # Multi-variant button component
│ │ ├── Logo.tsx # App logo component
│ │ └── RecentActivities.tsx # Activity tracking
│ ├── navigation/ # Navigation components
│ │ └── Sidebar.tsx # App sidebar
│ ├── ui/ # UI elements
│ │ └── IconSymbol.tsx # Icon components
│ └── auth/ # Auth-specific components
├── api/ # API integration files
│ ├── SignInApi.ts # Authentication APIs
│ ├── GetCoursesApi.ts # Course APIs
│ ├── GoalsApi.ts # Goals APIs
│ ├── ResumeApi.ts # Resume APIs
│ ├── MockInterviewApi.ts # Interview APIs
│ └── SkillMappingApi.ts # Skill mapping APIs
├── store/ # State management
│ ├── authStore.ts # Authentication state
│ ├── coursesStore.ts # Courses state
│ ├── goalsStore.ts # Goals state
│ └── resumeStore.ts # Resume state
├── styles/
│ ├── theme.ts # Design system & colors
│ ├── globalStyles.ts # Global styles & auth styles
│ └── responsive.ts # Responsive design utilities
└── constants/
└── Colors.ts # Theme colors
backend/auth-service/
├── controllers/ # Business logic
│ ├── authController.js # Authentication
│ ├── courseController.js # Course management
│ ├── goalsController.js # Goal tracking
│ ├── notesController.js # Notes system
│ ├── resumeController.js # Resume builder
│ ├── mockInterviewController.js # Interview system
│ ├── skillMappingController.js # Skill mapping
│ ├── groupController.js # Community features
│ ├── recommendationsController.js # AI recommendations
│ ├── recentActivityController.js # Activity tracking
│ └── parentDashboardController.js # Parent features
├── models/ # Database schemas
│ ├── User.models.js # User data
│ ├── Course.models.js # Course information
│ ├── Goals.models.js # Learning goals
│ ├── Notes.models.js # User notes
│ ├── Resume.models.js # Resume data
│ ├── MockInterview.models.js # Interview templates
│ ├── HistoryUserMockInterview.models.js # Interview history
│ ├── SkillMapping.models.js # Skill requirements
│ ├── Group.models.js # Study groups
│ ├── Enrollment.models.js # Course enrollments
│ └── RecentActivity.models.js # Activity logs
├── routes/ # API endpoints
│ ├── authRoutes.js # Authentication routes
│ ├── courseRoutes.js # Course routes
│ ├── goalsRoutes.js # Goals routes
│ ├── notesRoutes.js # Notes routes
│ ├── resumeRoutes.js # Resume routes
│ ├── mockInterviewRoutes.js # Interview routes
│ └── skillMappingRoutes.js # Skill mapping routes
├── middleware/
│ └── authMiddleware.js # JWT verification
├── utils/
│ └── resumeUtils.js # Resume generation utilities
├── config/
│ └── db.js # Database connection
└── scripts/ # Database setup scripts
├── importCourses.js # Course data import
└── populateInterviews.js # Interview templates
POST /api/v1/auth/register # User registration
POST /api/v1/auth/login # User login
GET /api/v1/auth/profile # Get user profile
GET /api/v1/courses/get-courses # Get all courses
GET /api/v1/courses/get-enrollments-courses # Get user enrollments
POST /api/v1/courses/enroll-course # Enroll in course
POST /api/v1/goals/ # Create new goal
GET /api/v1/goals/ # Get user goals
GET /api/v1/goals/stats # Get goal statistics
PUT /api/v1/goals/:goalId # Update goal
PATCH /api/v1/goals/:goalId/progress # Update progress
DELETE /api/v1/goals/:goalId # Delete goal
POST /api/v1/resume/generate # Generate AI-powered resume
GET /api/v1/resume/list # Get user resumes
GET /api/v1/resume/:resumeId # Get specific resume
GET /api/v1/resume/:resumeId/download # Download PDF
PUT /api/v1/resume/:resumeId # Update resume
DELETE /api/v1/resume/:resumeId # Delete resume
GET /api/v1/mock-interviews/ # Get available interviews
POST /api/v1/mock-interviews/start/:interviewId # Start interview
POST /api/v1/mock-interviews/answer/:interviewId # Submit answer
POST /api/v1/mock-interviews/complete/:interviewId # Complete interview
GET /api/v1/mock-interviews/history # Get interview history
GET /api/v1/mock-interviews/result/:historyId # Get result details
GET /api/v1/skill-mapping/companies # Get companies
GET /api/v1/skill-mapping/roles/:company # Get roles by company
GET /api/v1/skill-mapping/mappings # Get skill mappings
POST /api/v1/skill-mapping/recommendations # Get AI recommendations
PUT /api/v1/skill-mapping/user/skills # Update user skills
GET /api/v1/notes/get-notes # Get course notes
POST /api/v1/notes/add-notes # Add new note
PUT /api/v1/notes/update-note # Update existing note
DELETE /api/v1/notes/delete-note # Delete note
GET /api/v1/groups/get-groups # Get available groups
GET /api/v1/groups/joined-groups # Get user's groups
POST /api/v1/groups/create-group # Create new group
POST /api/v1/groups/join-group # Join existing group
POST /api/v1/groups/send-chat # Send group message
GET /api/v1/groups/get-chat # Get group chats
POST /api/v1/recommendations/generate # Get AI course recommendations
GET /api/v1/recent-activities/user-activities # Get user activities
GET /api/v1/parent-dashboard/overview # Parent dashboard data
- User.models.js - User authentication and profile data
- Parents.models.js - Parent/guardian access control
- Course.models.js - Course catalog and metadata
- Enrollment.models.js - User course enrollments
- Goals.models.js - Learning goals and progress tracking
- Notes.models.js - Course-specific user notes
- Resume.models.js - Resume data and versions
- MockInterview.models.js - Interview question templates
- HistoryUserMockInterview.models.js - Interview attempt history
- SkillMapping.models.js - Company skill requirements
- Group.models.js - Study group information
- GroupChats.models.js - Group messaging system
- Members.models.js - Group membership data
- RecentActivity.models.js - User activity logging
- Quizzes.models.js - Assessment and quiz data
theme = {
colors: {
primary: '#4F46E5', // Indigo for buttons/links
background: '#FFFFFF', // White background
surface: '#F9FAFB', // Light gray for inputs
text: '#111827', // Dark text
error: '#EF4444', // Red for errors
},
spacing: {
xs: 4, sm: 8, md: 16, lg: 24, xl: 32
},
borderRadius: {
sm: 4, md: 8, lg: 12, xl: 16
},
shadows: {
sm: { shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1 },
md: { shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.15 }
}
}- Button Component - Multiple variants (primary, secondary, outline, ghost)
- Input Component - With validation, icons, and error states
- Logo Component - Scalable app branding
- Sidebar Component - Navigation drawer
- RecentActivities Component - Activity feed display
- responsive.ts - Screen size utilities and responsive helpers
- Grid layouts that adapt to different screen sizes
- Scalable fonts and spacing for various devices
// AI Model Configuration
Model: Mistral (7B parameters)
Host: http://localhost:11434
Features:
- Course recommendations based on user goals
- Resume content generation and enhancement
- Mock interview evaluation with detailed feedback
- Skill gap analysis and learning path suggestions
- Career guidance and advice generation// Analyzes user goals, progress, and preferences
// Returns personalized course suggestions with reasoning
const recommendations = await generateRecommendations({
userGoals: userGoalData,
availableCourses: courseDatabase,
userPreferences: userProfile
});// Transforms basic user input into professional resume content
const enhancedResume = await generateResumeContent({
personalInfo: userData,
experience: workHistory,
education: educationData,
template: selectedTemplate
});// Provides detailed feedback on interview performance
const evaluation = await evaluateInterview({
role: targetRole,
questions: interviewQuestions,
answers: userAnswers
});
// Returns: technical_score, communication_score, feedback, strengths, weaknesses// Analyzes skill gaps for target roles
const skillAnalysis = await analyzeSkillGap({
targetRole: desiredPosition,
company: targetCompany,
currentSkills: userSkills,
requiredSkills: jobRequirements
});Our app implements a sophisticated offline-first architecture with intelligent sync mechanisms using AsyncStorage and Zustand state management. Here's how we handle data persistence and synchronization:
📱 Local Storage (AsyncStorage)
↕️ Bidirectional Sync
🧠 App State (Zustand Stores)
↕️ API Calls
☁️ Backend Server (MongoDB)- All data is stored locally first using AsyncStorage
- User actions work immediately even without internet
- Background sync happens when network is available
- Conflict resolution prioritizes user's local changes
type AuthState = {
// Core auth data (persisted)
isAuth: boolean;
userData: UserData | null;
token: string | null;
isLoading: boolean;
// Sync methods
login: (user: UserData, token: string) => void;
logout: () => void; // Clears all stores
updateUser: (patch: Partial<UserData>) => void;
}
// Persisted to AsyncStorage: isAuth, userData, tokentype CoursesState = {
// Core data (persisted)
courses: Course[];
enrolledCourses: string[];
// Offline sync variables
pendingEnrollments: PendingEnrollment[]; // 🔄 Enrollments waiting for sync
isSyncing: boolean; // 📡 Currently syncing status
isEnrolling: boolean; // ⏳ Currently enrolling status
isOnline: boolean; // 📶 Network connectivity status
lastSyncedAt: string | null; // 🕐 Last successful sync timestamp
// Sync methods
fetchAndSync: () => Promise<void>; // 🔄 Main sync function
enrollCourse: () => Promise<{success, message}>; // 📝 Offline-capable enrollment
syncPendingEnrollments: () => Promise<void>; // 🚀 Sync pending items
setOnlineStatus: (isOnline: boolean) => void; // 📶 Network status updates
}
// Sync Logic Flow:
// 1. User enrolls → Add to pendingEnrollments if offline
// 2. UI updates immediately (optimistic updates)
// 3. When online → syncPendingEnrollments() syncs to server
// 4. Success → Move to enrolledCourses, remove from pendingtype CourseDetailState = {
// Core data (persisted)
notesByCourse: Record<string, Note[]>;
quizzesByCourse: Record<string, Quiz[]>;
progressByCourse: Record<string, CourseProgress>;
// Offline sync variables
pendingNotes: PendingNote[]; // 📝 Notes waiting for server sync
pendingSync: { // 🔄 Generic pending sync data
notes: Record<string, Note[]>;
progress: Record<string, CourseProgress>;
};
isOnline: boolean; // 📶 Network status
isSyncingNotes: boolean; // 📤 Notes sync in progress
// Sync methods
saveNoteToServer: () => Promise<{success, message}>; // 💾 Offline-capable note saving
addPendingNote: (courseId, content) => void; // 📝 Add to offline queue
syncPendingNotes: () => Promise<void>; // 🚀 Sync all pending notes
addToPendingSync: () => void; // 🔄 Generic sync queue
}
// Notes Sync Logic:
// 1. User saves note → Try server first if online
// 2. If offline/error → Add to pendingNotes
// 3. UI shows note immediately with "syncing" indicator
// 4. When online → syncPendingNotes() uploads all pending
// 5. Success → Remove from pendingNotestype GoalsState = {
// Core data (persisted)
goals: Goal[];
goalStats: GoalStats | null;
// UI state (persisted)
statusFilter: string | null; // 🔍 Filter persistence
priorityFilter: string | null; // 🔍 Filter persistence
// Loading states
isLoading: boolean;
error: string | null;
// Computed getters (no network dependency)
getFilteredGoals: () => Goal[]; // 📊 Local filtering
getGoalsByStatus: (status) => Goal[]; // 📊 Local queries
getOverdueGoals: () => Goal[]; // 📊 Local calculations
}
// Goals work offline with local state management
// Server sync happens via API calls, local state updates immediatelytype ResumeState = {
// Complete resume data (persisted)
resumeData: ResumeData; // 📄 All resume information
isGenerating: boolean; // 🤖 AI generation status
lastGenerated: string; // 🕐 Last generation timestamp
// Rich data structures
education: Education[]; // 🎓 Education history
experience: Experience[]; // 💼 Work experience
projects: Project[]; // 🚀 Personal projects
achievements: Achievement[]; // 🏆 Accomplishments
// Local operations (work offline)
getFormProgress: () => number; // 📊 Completion percentage
isFormValid: () => boolean; // ✅ Validation check
}
// Resume builder works completely offline
// Only PDF generation requires server (with local fallback)// Example: Course Enrollment
const enrollCourse = async (courseId: string) => {
// 1. Update UI immediately (optimistic)
const updatedCourses = courses.map(course =>
course.id === courseId ? { ...course, enrolled: true } : course
);
setCourses(updatedCourses);
// 2. Try server sync
if (isOnline && token) {
try {
await EnrollCourseApi(token, courseId);
// Success - already updated UI
} catch (error) {
// Error - add to pending queue
addPendingEnrollment(courseId);
}
} else {
// Offline - add to pending queue
addPendingEnrollment(courseId);
}
};// Pending items structure
type PendingEnrollment = {
courseId: string;
timestamp: string; // For conflict resolution
};
type PendingNote = {
courseId: string;
content: string;
timestamp: string;
};
// Auto-sync when network returns
const setOnlineStatus = (isOnline: boolean) => {
setIsOnline(isOnline);
if (isOnline && token) {
// Auto-trigger sync for all pending items
syncPendingEnrollments(token);
syncPendingNotes(token);
}
};// What gets persisted to AsyncStorage:
const persistConfig = {
name: 'store-name',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({
// Core data - always persisted
courses: state.courses,
enrolledCourses: state.enrolledCourses,
goals: state.goals,
// Sync state - persisted for offline resilience
pendingEnrollments: state.pendingEnrollments,
pendingNotes: state.pendingNotes,
lastSyncedAt: state.lastSyncedAt,
// UI state - persisted for UX
statusFilter: state.statusFilter,
priorityFilter: state.priorityFilter,
}),
};// Multi-layer network checking approach
const checkNetworkStatus = async () => {
// Layer 1: Browser network status
const browserOnline = navigator.onLine ?? true;
if (!browserOnline) {
console.log('Browser says offline');
setOnlineStatus(false);
return;
}
// Layer 2: Backend API verification
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3s timeout
const apiResponse = await fetch('http://192.168.0.102:5000/', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal
});
clearTimeout(timeoutId);
if (apiResponse?.status === 200 || apiResponse?.status === 201) {
console.log('Backend API check: Online ✅');
setOnlineStatus(true);
} else {
console.log('Backend API check: Failed ❌');
setOnlineStatus(false);
}
} catch (error) {
// Network error or timeout - consider offline
if (error.name === 'AbortError') {
console.warn('Backend API check: Timeout ⏰');
} else {
console.warn('Backend API check: Network error 🔌');
}
setOnlineStatus(false);
}
};useEffect(() => {
// Initial network status check
checkNetworkStatus();
// Browser event listeners for immediate response
const handleOnline = () => {
console.log('🔔 Browser detected online - rechecking backend');
checkNetworkStatus(); // Verify with backend
};
const handleOffline = () => {
console.log('🔔 Browser detected offline');
setOnlineStatus(false);
};
// Add event listeners
window.addEventListener?.('online', handleOnline);
window.addEventListener?.('offline', handleOffline);
// 🔄 Periodic polling every 30 seconds
const intervalId = setInterval(() => {
console.log('⏰ 30-second network check triggered');
checkNetworkStatus();
}, 30000);
// Cleanup on component unmount
return () => {
window.removeEventListener?.('online', handleOnline);
window.removeEventListener?.('offline', handleOffline);
clearInterval(intervalId);
};
}, [setOnlineStatus]);Immediate Triggers:
// 1. Browser online/offline events (instant response)
window.addEventListener('online', () => {
console.log('📶 Network restored - immediate sync check');
checkNetworkStatus();
});
// 2. App foreground detection
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'active' && isOnline && token) {
console.log('📱 App foregrounded - checking for sync');
const lastSync = coursesStore.lastSyncedAt;
const timeSinceSync = Date.now() - new Date(lastSync).getTime();
// Sync if more than 5 minutes since last sync
if (timeSinceSync > 5 * 60 * 1000) {
console.log('🔄 Auto-syncing after 5+ minutes');
coursesStore.fetchAndSync(token);
}
}
});
return subscription.remove;
}, []);Scheduled Triggers:
// 30-second polling for network status
setInterval(checkNetworkStatus, 30000);
// Smart sync scheduling based on app usage
useEffect(() => {
if (isOnline && token) {
// Schedule background sync every 2 minutes when app is active
const syncInterval = setInterval(() => {
console.log('📡 Scheduled background sync');
// Only sync if there are pending changes
const pendingItems = getPendingItemsCount();
if (pendingItems > 0) {
console.log(`🔄 Syncing ${pendingItems} pending items`);
syncAllPendingData(token);
}
}, 2 * 60 * 1000); // 2 minutes
return () => clearInterval(syncInterval);
}
}, [isOnline, token]);// Dynamic polling based on network reliability
let pollInterval = 30000; // Default 30 seconds
let consecutiveFailures = 0;
const adaptivePoll = () => {
checkNetworkStatus()
.then((isOnline) => {
if (isOnline) {
// Network is stable - reset to normal polling
consecutiveFailures = 0;
pollInterval = 30000; // 30 seconds
console.log('📶 Network stable - normal polling (30s)');
} else {
// Network issues - increase polling frequency temporarily
consecutiveFailures++;
if (consecutiveFailures > 3) {
pollInterval = 60000; // 1 minute if persistently offline
console.log('⚠️ Persistent offline - reduced polling (60s)');
} else {
pollInterval = 15000; // 15 seconds for quick recovery
console.log('🔄 Network issues - fast polling (15s)');
}
}
});
};
// Use adaptive polling
setInterval(adaptivePoll, pollInterval);// Optimize polling based on device state
const getOptimalPollInterval = () => {
// Check if app is in background (React Native)
if (AppState.currentState === 'background') {
return 5 * 60 * 1000; // 5 minutes in background
}
// Check battery level (if available)
if (getBatteryLevel && getBatteryLevel() < 0.2) {
return 2 * 60 * 1000; // 2 minutes on low battery
}
// Check if device is charging
if (getBatteryCharging && getBatteryCharging()) {
return 15 * 1000; // 15 seconds when charging
}
return 30 * 1000; // Default 30 seconds
};
// Dynamic interval adjustment
let currentInterval = getOptimalPollInterval();
setInterval(() => {
const newInterval = getOptimalPollInterval();
if (newInterval !== currentInterval) {
console.log(`🔄 Adjusting poll interval: ${currentInterval}ms → ${newInterval}ms`);
currentInterval = newInterval;
// Restart interval with new timing
}
}, currentInterval);// Measure network quality for better sync decisions
const assessNetworkQuality = async () => {
const startTime = Date.now();
try {
// Small test request to measure latency
await fetch('http://192.168.0.102:5000/api/v1/health', {
method: 'HEAD',
timeout: 2000
});
const latency = Date.now() - startTime;
if (latency < 500) {
console.log('🚀 Excellent network (< 500ms)');
return 'excellent';
} else if (latency < 1500) {
console.log('⚡ Good network (500-1500ms)');
return 'good';
} else {
console.log('🐌 Slow network (> 1500ms)');
return 'slow';
}
} catch (error) {
console.log('❌ Poor/No network');
return 'poor';
}
};
// Adjust sync behavior based on network quality
const smartSync = async () => {
const quality = await assessNetworkQuality();
switch (quality) {
case 'excellent':
// Sync everything immediately
await syncAllData();
break;
case 'good':
// Prioritize important data
await syncPriorityData();
setTimeout(syncRemainingData, 1000);
break;
case 'slow':
// Sync only critical data
await syncCriticalData();
break;
case 'poor':
// Queue for later
console.log('📦 Queuing data for better network');
break;
}
};// Visual network status indicators
const NetworkStatusIndicator = () => {
const isOnline = useCoursesStore(s => s.isOnline);
const isSyncing = useCoursesStore(s => s.isSyncing);
const lastSyncedAt = useCoursesStore(s => s.lastSyncedAt);
return (
<View style={styles.networkIndicator}>
{isSyncing ? (
<Text style={[styles.statusText, { color: colors.warning }]}>
🔄 Syncing...
</Text>
) : isOnline ? (
<Text style={[styles.statusText, { color: colors.success }]}>
✅ Online {lastSyncedAt && `• Last sync: ${formatTime(lastSyncedAt)}`}
</Text>
) : (
<Text style={[styles.statusText, { color: colors.error }]}>
⚠️ Offline • Changes saved locally
</Text>
)}
</View>
);
};// Track sync performance for optimization
const syncMetrics = {
totalSyncAttempts: 0,
successfulSyncs: 0,
failedSyncs: 0,
averageLatency: 0,
lastSyncTime: null,
recordSyncAttempt: (success: boolean, latency: number) => {
syncMetrics.totalSyncAttempts++;
if (success) {
syncMetrics.successfulSyncs++;
syncMetrics.lastSyncTime = Date.now();
} else {
syncMetrics.failedSyncs++;
}
// Update average latency
syncMetrics.averageLatency =
(syncMetrics.averageLatency * (syncMetrics.totalSyncAttempts - 1) + latency) /
syncMetrics.totalSyncAttempts;
},
getSyncHealth: () => {
const successRate = (syncMetrics.successfulSyncs / syncMetrics.totalSyncAttempts) * 100;
console.log(`📊 Sync Health: ${successRate.toFixed(1)}% success rate, ${syncMetrics.averageLatency.toFixed(0)}ms avg latency`);
return { successRate, averageLatency: syncMetrics.averageLatency };
}
};This sophisticated polling and sync system ensures reliable offline/online detection with:
✅ 30-second automatic polling for network status
✅ Immediate browser event detection for instant response
✅ Backend API verification beyond just browser status
✅ Adaptive polling frequency based on network reliability
✅ Battery and performance optimization
✅ Network quality assessment for smart sync decisions
✅ Visual feedback to users about connection status
✅ Comprehensive error handling with fallback strategies
// Each store tracks online status
isOnline: boolean; // 📶 Current network state
// Auto-sync triggers
const setOnlineStatus = (isOnline: boolean) => {
set({ isOnline });
if (isOnline) {
// Coming online - trigger sync
const { pendingItems } = get();
if (pendingItems.length > 0) {
console.log('📶 Network restored - syncing pending items');
syncPendingItems();
}
}
};// Loading states for user feedback
isSyncing: boolean; // 🔄 General sync in progress
isSyncingNotes: boolean; // 📝 Notes sync in progress
isEnrolling: boolean; // 📚 Enrollment in progress
// UI shows appropriate indicators:
// - "Syncing..." badge
// - Progress spinners
// - "Saved offline" messages// On app startup
useEffect(() => {
if (token) {
// Sync all stores
coursesStore.fetchAndSync(token);
courseDetailStore.syncPendingNotes(token);
// ... other stores
}
}, [token]);// Network state listener
useEffect(() => {
const networkListener = NetInfo.addEventListener(state => {
const isOnline = state.isConnected && state.isInternetReachable;
// Update all stores
coursesStore.setOnlineStatus(isOnline);
courseDetailStore.setOnlineStatus(isOnline);
});
return networkListener;
}, []);// App state change listener
useEffect(() => {
const subscription = AppState.addEventListener('change', nextAppState => {
if (nextAppState === 'active' && isOnline && token) {
// App came to foreground - sync if needed
const lastSync = coursesStore.lastSyncedAt;
const timeSinceSync = Date.now() - new Date(lastSync).getTime();
if (timeSinceSync > 5 * 60 * 1000) { // 5 minutes
console.log('🔄 App foregrounded - syncing data');
coursesStore.fetchAndSync(token);
}
}
});
return subscription.remove;
}, []);// Resolve conflicts using timestamps
const resolveConflict = (localItem, serverItem) => {
const localTime = new Date(localItem.updatedAt);
const serverTime = new Date(serverItem.updatedAt);
// Prioritize most recent change
return localTime > serverTime ? localItem : serverItem;
};// For critical actions, always prioritize user's local changes
const mergeCourseProgress = (local, server) => {
return {
...server,
completedVideos: [
...new Set([...server.completedVideos, ...local.completedVideos])
],
progressPercentage: Math.max(local.progressPercentage, server.progressPercentage),
lastWatchedVideo: Math.max(local.lastWatchedVideo, server.lastWatchedVideo),
};
};This offline/online sync architecture ensures that users can always use the app regardless of network connectivity, with seamless data synchronization when the connection is restored.