Spaces:
Sleeping
Sleeping
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 +51 -49
- client/src/pages/Profile.tsx +7 -1
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 |
-
|
| 30 |
useEffect(() => {
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
}, transitionDuration);
|
| 77 |
-
|
| 78 |
-
return () => clearTimeout(timer);
|
| 79 |
}
|
| 80 |
-
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|