Skip to content

Commit 2bcdb32

Browse files
feat: implement Phase 2 Core LMS APIs (#811)
* feat: implement Phase 2 Core LMS APIs * Update src/pages/api/courses/index.ts Co-authored-by: Copilot <[email protected]> * Update src/pages/api/progress/index.ts Co-authored-by: Copilot <[email protected]> * Update src/pages/api/enrollment/index.ts Co-authored-by: Copilot <[email protected]> * Update src/pages/api/enrollment/index.ts Co-authored-by: Copilot <[email protected]> * fix: correct Progress schema queries to use userId instead of enrollmentId --------- Co-authored-by: Copilot <[email protected]>
1 parent a2239c1 commit 2bcdb32

File tree

9 files changed

+2759
-120
lines changed

9 files changed

+2759
-120
lines changed

docs/phase-2-core-apis-plan.md

Lines changed: 1651 additions & 0 deletions
Large diffs are not rendered by default.

src/hooks/useOfflineProgress.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { useEffect, useState } from 'react';
2+
import {
3+
saveOfflineProgress,
4+
syncOfflineProgress,
5+
getOfflineQueue,
6+
isOnline,
7+
} from '@/lib/offline-sync';
8+
9+
export function useOfflineProgress() {
10+
const [pendingSync, setPendingSync] = useState(0);
11+
const [isSyncing, setIsSyncing] = useState(false);
12+
const [online, setOnline] = useState(true);
13+
14+
useEffect(() => {
15+
// Update pending count and online status
16+
const updateStatus = () => {
17+
setPendingSync(getOfflineQueue().length);
18+
setOnline(isOnline());
19+
};
20+
21+
updateStatus();
22+
23+
// Auto-sync when coming online
24+
const handleOnline = async () => {
25+
setOnline(true);
26+
if (getOfflineQueue().length > 0) {
27+
await handleSync();
28+
}
29+
};
30+
31+
const handleOffline = () => {
32+
setOnline(false);
33+
};
34+
35+
window.addEventListener('online', handleOnline);
36+
window.addEventListener('offline', handleOffline);
37+
window.addEventListener('storage', updateStatus);
38+
39+
return () => {
40+
window.removeEventListener('online', handleOnline);
41+
window.removeEventListener('offline', handleOffline);
42+
window.removeEventListener('storage', updateStatus);
43+
};
44+
}, []);
45+
46+
const updateProgress = async (
47+
enrollmentId: string,
48+
lessonId: string,
49+
completed: boolean,
50+
timeSpent: number
51+
) => {
52+
if (!isOnline()) {
53+
// Save offline
54+
saveOfflineProgress({
55+
enrollmentId,
56+
lessonId,
57+
completed,
58+
timeSpent,
59+
});
60+
setPendingSync(getOfflineQueue().length);
61+
return { offline: true };
62+
}
63+
64+
// Try to sync online
65+
try {
66+
const response = await fetch('/api/progress', {
67+
method: 'POST',
68+
headers: {
69+
'Content-Type': 'application/json',
70+
},
71+
body: JSON.stringify({
72+
enrollmentId,
73+
lessonId,
74+
completed,
75+
timeSpent,
76+
}),
77+
});
78+
79+
if (!response.ok) {
80+
throw new Error('Failed to update progress');
81+
}
82+
83+
return await response.json();
84+
} catch (error) {
85+
// Fallback to offline
86+
saveOfflineProgress({
87+
enrollmentId,
88+
lessonId,
89+
completed,
90+
timeSpent,
91+
});
92+
setPendingSync(getOfflineQueue().length);
93+
return { offline: true, error };
94+
}
95+
};
96+
97+
const handleSync = async () => {
98+
setIsSyncing(true);
99+
try {
100+
const results = await syncOfflineProgress();
101+
setPendingSync(getOfflineQueue().length);
102+
return results;
103+
} finally {
104+
setIsSyncing(false);
105+
}
106+
};
107+
108+
return {
109+
updateProgress,
110+
syncOfflineProgress: handleSync,
111+
pendingSync,
112+
isSyncing,
113+
isOnline: online,
114+
};
115+
}

src/lib/offline-sync.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
export interface OfflineProgress {
2+
enrollmentId: string;
3+
lessonId: string;
4+
completed: boolean;
5+
timeSpent: number;
6+
timestamp: number;
7+
}
8+
9+
const STORAGE_KEY = 'vwc_offline_progress';
10+
11+
/**
12+
* Save progress to localStorage when offline
13+
*/
14+
export function saveOfflineProgress(progress: Omit<OfflineProgress, 'timestamp'>): void {
15+
try {
16+
const offlineQueue = getOfflineQueue();
17+
18+
// Add timestamp
19+
const progressWithTimestamp: OfflineProgress = {
20+
...progress,
21+
timestamp: Date.now(),
22+
};
23+
24+
// Check if this lesson progress already exists in queue
25+
const existingIndex = offlineQueue.findIndex(
26+
(p) => p.enrollmentId === progress.enrollmentId && p.lessonId === progress.lessonId
27+
);
28+
29+
if (existingIndex >= 0) {
30+
// Update existing record
31+
offlineQueue[existingIndex] = progressWithTimestamp;
32+
} else {
33+
// Add new record
34+
offlineQueue.push(progressWithTimestamp);
35+
}
36+
37+
localStorage.setItem(STORAGE_KEY, JSON.stringify(offlineQueue));
38+
} catch (error) {
39+
console.error('Failed to save offline progress:', error);
40+
}
41+
}
42+
43+
/**
44+
* Get all offline progress records
45+
*/
46+
export function getOfflineQueue(): OfflineProgress[] {
47+
try {
48+
const data = localStorage.getItem(STORAGE_KEY);
49+
return data ? JSON.parse(data) : [];
50+
} catch (error) {
51+
console.error('Failed to get offline queue:', error);
52+
return [];
53+
}
54+
}
55+
56+
/**
57+
* Clear offline progress queue
58+
*/
59+
export function clearOfflineQueue(): void {
60+
try {
61+
localStorage.removeItem(STORAGE_KEY);
62+
} catch (error) {
63+
console.error('Failed to clear offline queue:', error);
64+
}
65+
}
66+
67+
/**
68+
* Sync offline progress to server
69+
*/
70+
export async function syncOfflineProgress(): Promise<{
71+
success: number;
72+
failed: number;
73+
errors: any[];
74+
}> {
75+
const queue = getOfflineQueue();
76+
77+
if (queue.length === 0) {
78+
return { success: 0, failed: 0, errors: [] };
79+
}
80+
81+
const results = {
82+
success: 0,
83+
failed: 0,
84+
errors: [] as any[],
85+
};
86+
87+
// Process each progress record
88+
for (const progress of queue) {
89+
try {
90+
const response = await fetch('/api/progress', {
91+
method: 'POST',
92+
headers: {
93+
'Content-Type': 'application/json',
94+
},
95+
body: JSON.stringify({
96+
enrollmentId: progress.enrollmentId,
97+
lessonId: progress.lessonId,
98+
completed: progress.completed,
99+
timeSpent: progress.timeSpent,
100+
}),
101+
});
102+
103+
if (response.ok) {
104+
results.success++;
105+
} else {
106+
results.failed++;
107+
results.errors.push({
108+
progress,
109+
error: await response.text(),
110+
});
111+
}
112+
} catch (error) {
113+
results.failed++;
114+
results.errors.push({ progress, error });
115+
}
116+
}
117+
118+
// Clear queue if all synced successfully
119+
if (results.failed === 0) {
120+
clearOfflineQueue();
121+
}
122+
123+
return results;
124+
}
125+
126+
/**
127+
* Check if browser is online
128+
*/
129+
export function isOnline(): boolean {
130+
return typeof navigator !== 'undefined' ? navigator.onLine : true;
131+
}

0 commit comments

Comments
 (0)