Tristan Yu commited on
Commit
4fd7cee
·
1 Parent(s): 267cbf7

Presence: frontend heartbeat + green dot indicator in Manage users list

Browse files
client/src/components/Layout.tsx CHANGED
@@ -26,58 +26,60 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
26
  const user: User | null = userData ? JSON.parse(userData) : null;
27
  const [unreadCount, setUnreadCount] = useState<number>(0);
28
 
29
- // Handle page transitions
30
  useEffect(() => {
31
- // Only trigger transition if path actually changed
32
- if (location.pathname !== previousPathRef.current) {
33
- setIsTransitioning(true);
34
- previousPathRef.current = location.pathname;
35
-
36
- // Reset week selection to Week 1 when navigating between pages
37
- const previousPath = previousPathRef.current;
38
- if (location.pathname === '/tutorial-tasks' &&
39
- previousPath &&
40
- !previousPath.includes('/tutorial-tasks')) {
41
- // Use URL parameter to force Week 1
42
- window.history.replaceState(null, '', '/tutorial-tasks?week=1');
43
- localStorage.setItem('selectedTutorialWeek', '1');
44
- } else if (location.pathname === '/weekly-practice' &&
45
- previousPath &&
46
- !previousPath.includes('/weekly-practice')) {
47
- // Use URL parameter to force Week 1
48
- window.history.replaceState(null, '', '/weekly-practice?week=1');
49
- localStorage.setItem('selectedWeeklyPracticeWeek', '1');
50
- }
51
-
52
- // Determine transition duration based on destination page
53
- let transitionDuration = 800; // Default duration
54
-
55
- // Longer duration for heavy pages
56
- if (location.pathname === '/tutorial-tasks' || location.pathname === '/weekly-practice') {
57
- transitionDuration = 1200; // Longer for content-heavy pages
58
- }
59
-
60
- // Special case: Weekly Practice → Tutorial Tasks (add 500ms delay)
61
- if (location.pathname === '/tutorial-tasks' &&
62
- previousPath &&
63
- previousPath.includes('/weekly-practice')) {
64
- // Check if navigating to Week 2 (use localStorage since URL might not be updated yet)
65
- const tutorialWeek = localStorage.getItem('selectedTutorialWeek');
66
- if (tutorialWeek === '2') {
67
- transitionDuration = 2500; // Extended duration for Week 2 (2000 + 500)
68
- } else {
69
- transitionDuration = 1700; // Standard duration for Week 1 (1200 + 500)
 
 
 
70
  }
71
- }
72
-
73
- // End transition after content is loaded (wait for DOM updates)
74
- const timer = setTimeout(() => {
75
- setIsTransitioning(false);
76
- }, transitionDuration);
77
-
78
- return () => clearTimeout(timer);
79
  }
80
- }, [location.pathname]);
 
 
81
 
82
  // Admin unread message badge (non-invasive)
83
  useEffect(() => {
 
26
  const user: User | null = userData ? JSON.parse(userData) : null;
27
  const [unreadCount, setUnreadCount] = useState<number>(0);
28
 
29
+ // Lightweight online presence: send heartbeat periodically
30
  useEffect(() => {
31
+ let timer: any;
32
+ const sendHeartbeat = async () => {
33
+ try {
34
+ if (!user?.email) return;
35
+ const token = localStorage.getItem('token') || '';
36
+ const base = (((api.defaults as any)?.baseURL as string) || '').replace(/\/$/, '');
37
+ await fetch(`${base}/api/auth/online/heartbeat`, {
38
+ method: 'POST',
39
+ headers: {
40
+ 'Authorization': `Bearer ${token}`,
41
+ 'Content-Type': 'application/json',
42
+ 'user-role': user.role || 'visitor',
43
+ 'user-info': userData || ''
44
+ },
45
+ body: JSON.stringify({ email: user.email })
46
+ });
47
+ } catch {}
48
+ };
49
+ sendHeartbeat();
50
+ timer = setInterval(sendHeartbeat, 60000);
51
+ return () => { if (timer) clearInterval(timer); };
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ }, [user?.email]);
54
+
55
+ // Admin unread message badge (non-invasive)
56
+ useEffect(() => {
57
+ let timer: any;
58
+ const run = async () => {
59
+ try {
60
+ if (user?.role !== 'admin') return;
61
+ const token = localStorage.getItem('token') || '';
62
+ const base = (((api.defaults as any)?.baseURL as string) || '').replace(/\/$/, '');
63
+ const resp = await fetch(`${base}/api/messages/unread-count`, {
64
+ headers: {
65
+ 'Authorization': `Bearer ${token}`,
66
+ 'user-role': 'admin',
67
+ 'user-info': userData || ''
68
+ }
69
+ });
70
+ if (resp.ok) {
71
+ const data = await resp.json();
72
+ if (typeof data?.count === 'number') setUnreadCount(data.count);
73
  }
74
+ } catch {}
75
+ };
76
+ run();
77
+ if (user?.role === 'admin') {
78
+ timer = setInterval(run, 60000);
 
 
 
79
  }
80
+ return () => { if (timer) clearInterval(timer); };
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ }, [user?.role]);
83
 
84
  // Admin unread message badge (non-invasive)
85
  useEffect(() => {
client/src/pages/Profile.tsx CHANGED
@@ -16,6 +16,7 @@ interface User {
16
  email: string;
17
  role?: string;
18
  displayName?: string;
 
19
  }
20
 
21
  interface SystemStats {
@@ -537,7 +538,12 @@ const Manage: React.FC = () => {
537
  <div key={user.email} className="bg-gray-50 p-3 rounded-md">
538
  <div className="flex justify-between items-center">
539
  <div className="flex-1">
540
- <p className="text-sm font-medium text-gray-900">{user.name}</p>
 
 
 
 
 
541
  <p className="text-xs text-gray-600">{user.email}</p>
542
  </div>
543
  <div className="flex items-center space-x-2">
 
16
  email: string;
17
  role?: string;
18
  displayName?: string;
19
+ online?: boolean;
20
  }
21
 
22
  interface SystemStats {
 
538
  <div key={user.email} className="bg-gray-50 p-3 rounded-md">
539
  <div className="flex justify-between items-center">
540
  <div className="flex-1">
541
+ <p className="text-sm font-medium text-gray-900 flex items-center">
542
+ {user.online && (
543
+ <span className="inline-block w-2 h-2 bg-green-500 rounded-full mr-2" aria-label="online" />
544
+ )}
545
+ {user.name}
546
+ </p>
547
  <p className="text-xs text-gray-600">{user.email}</p>
548
  </div>
549
  <div className="flex items-center space-x-2">