feat: add online users counter#163
Conversation
…online counter on Home page - Add session tracking API endpoint with PostgreSQL storage - Display animated popup showing active users count - Show motivational messages based on user activity - Include automatic session cleanup (15-min timeout)
✅ Deploy Preview for develop-devlovers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThis PR introduces a real-time online user counter feature. A new API route tracks user sessions with activity timestamps, performs probabilistic cleanup of stale sessions, and returns the count of active users. A new React component fetches and displays this count with animations and visual styling. Changes
Sequence DiagramsequenceDiagram
participant User as User Browser
participant Layout as LocaleLayout
participant Component as OnlineCounterPopup
participant API as /api/sessions/activity
participant DB as activeSessions DB
User->>Layout: Load page
Layout->>Component: Mount OnlineCounterPopup
Component->>Component: Initialize state
Component->>API: POST /api/sessions/activity
API->>API: Retrieve/generate sessionId
API->>DB: Upsert session with timestamp
API->>DB: Delete old sessions (5% chance)
API->>DB: Count distinct sessions < 15min old
API->>API: Set secure HTTP-only cookie
API-->>Component: {online: count}
Component->>Component: Store count in state
Component->>Component: Set show flag after delay
Component->>User: Render animated popup with count
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@frontend/app/api/sessions/activity/route.ts`:
- Around line 30-38: The fire-and-forget cleanup call can be killed by
serverless request termination; modify the logic around
db.delete(activeSessions).where(lt(activeSessions.lastActivity,
cleanupThreshold)) so the cleanup is awaited (or moved to a scheduled/background
job instead) while still swallowing errors—i.e., await the delete promise and
catch/log errors (using the existing catch pattern) or extract this cleanup into
a cron/worker; reference the symbols activeSessions, lastActivity,
SESSION_TIMEOUT_MINUTES and db.delete to locate and update the code.
In `@frontend/components/shared/OnlineCounterPopup.tsx`:
- Around line 10-21: In OnlineCounterPopup's useEffect: remove the early return
that blocks the fetch so the POST to '/api/sessions/activity' always runs (so
lastActivity is refreshed), but still gate the UI show/hide using
sessionStorage.getItem('shown') when deciding whether to call setShow; store the
timeout IDs returned from setTimeout in local variables, use a cleanup function
to clear those timeouts and set an "unmounted" flag (or check mounted state) to
avoid calling setShow/setOnline after unmount, and ensure
sessionStorage.setItem('shown','1') is only used to prevent future UI displays,
not to skip the API ping.
- Line 24: The component currently hides the popup for falsy online counts;
update OnlineCounterPopup.tsx to use an explicit null check on the `online`
prop/variable (replace `if (!online) return null;` with `if (online === null)
return null;`) so that a valid value of 0 is rendered (and adjust the displayed
copy/emoji in the render path to correctly show "0 online" where appropriate).
| if (Math.random() < 0.05) { | ||
| const cleanupThreshold = new Date( | ||
| Date.now() - SESSION_TIMEOUT_MINUTES * 60 * 1000 | ||
| ); | ||
|
|
||
| db.delete(activeSessions) | ||
| .where(lt(activeSessions.lastActivity, cleanupThreshold)) | ||
| .catch(err => console.error('Cleanup error:', err)); | ||
| } |
There was a problem hiding this comment.
Cleanup may never run if the request ends early.
In serverless runtimes, un-awaited DB calls can be terminated when the response returns, leaving stale sessions uncollected and the table growing. Consider awaiting the cleanup (or moving it to a scheduled job) while still swallowing errors to keep the request resilient.
🛠️ Suggested fix (await cleanup safely)
- if (Math.random() < 0.05) {
- const cleanupThreshold = new Date(
- Date.now() - SESSION_TIMEOUT_MINUTES * 60 * 1000
- );
-
- db.delete(activeSessions)
- .where(lt(activeSessions.lastActivity, cleanupThreshold))
- .catch(err => console.error('Cleanup error:', err));
- }
+ if (Math.random() < 0.05) {
+ const cleanupThreshold = new Date(
+ Date.now() - SESSION_TIMEOUT_MINUTES * 60 * 1000
+ );
+
+ try {
+ await db
+ .delete(activeSessions)
+ .where(lt(activeSessions.lastActivity, cleanupThreshold));
+ } catch (err) {
+ console.error('Cleanup error:', err);
+ }
+ }🤖 Prompt for AI Agents
In `@frontend/app/api/sessions/activity/route.ts` around lines 30 - 38, The
fire-and-forget cleanup call can be killed by serverless request termination;
modify the logic around
db.delete(activeSessions).where(lt(activeSessions.lastActivity,
cleanupThreshold)) so the cleanup is awaited (or moved to a scheduled/background
job instead) while still swallowing errors—i.e., await the delete promise and
catch/log errors (using the existing catch pattern) or extract this cleanup into
a cron/worker; reference the symbols activeSessions, lastActivity,
SESSION_TIMEOUT_MINUTES and db.delete to locate and update the code.
| useEffect(() => { | ||
| if (sessionStorage.getItem('shown')) return; | ||
|
|
||
| fetch('/api/sessions/activity', { method: 'POST' }) | ||
| .then(r => r.json()) | ||
| .then(data => { | ||
| setOnline(data.online); | ||
| setTimeout(() => setShow(true), 500); | ||
| sessionStorage.setItem('shown', '1'); | ||
| setTimeout(() => setShow(false), 10000); | ||
| }) | ||
| .catch(() => setOnline(null)); |
There was a problem hiding this comment.
Session tracking stops after the first display in a tab.
The early sessionStorage return prevents subsequent POSTs, so lastActivity isn’t refreshed and long-lived sessions will fall out of the 15‑minute window even while the user is active. You can still call the API while gating only the UI display. Also consider cleaning up timeouts to avoid state updates after unmount.
🛠️ Suggested fix (always ping, gate UI, and clear timers)
- useEffect(() => {
- if (sessionStorage.getItem('shown')) return;
-
- fetch('/api/sessions/activity', { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- setOnline(data.online);
- setTimeout(() => setShow(true), 500);
- sessionStorage.setItem('shown', '1');
- setTimeout(() => setShow(false), 10000);
- })
- .catch(() => setOnline(null));
- }, []);
+ useEffect(() => {
+ const alreadyShown = sessionStorage.getItem('shown');
+ let showTimer: ReturnType<typeof setTimeout> | undefined;
+ let hideTimer: ReturnType<typeof setTimeout> | undefined;
+
+ fetch('/api/sessions/activity', { method: 'POST' })
+ .then(r => r.json())
+ .then(data => {
+ setOnline(data.online);
+ if (!alreadyShown) {
+ showTimer = setTimeout(() => setShow(true), 500);
+ sessionStorage.setItem('shown', '1');
+ hideTimer = setTimeout(() => setShow(false), 10000);
+ }
+ })
+ .catch(() => setOnline(null));
+
+ return () => {
+ if (showTimer) clearTimeout(showTimer);
+ if (hideTimer) clearTimeout(hideTimer);
+ };
+ }, []);🤖 Prompt for AI Agents
In `@frontend/components/shared/OnlineCounterPopup.tsx` around lines 10 - 21, In
OnlineCounterPopup's useEffect: remove the early return that blocks the fetch so
the POST to '/api/sessions/activity' always runs (so lastActivity is refreshed),
but still gate the UI show/hide using sessionStorage.getItem('shown') when
deciding whether to call setShow; store the timeout IDs returned from setTimeout
in local variables, use a cleanup function to clear those timeouts and set an
"unmounted" flag (or check mounted state) to avoid calling setShow/setOnline
after unmount, and ensure sessionStorage.setItem('shown','1') is only used to
prevent future UI displays, not to skip the API ping.
| .catch(() => setOnline(null)); | ||
| }, []); | ||
|
|
||
| if (!online) return null; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n frontend/components/shared/OnlineCounterPopup.tsxRepository: DevLoversTeam/devlovers.net
Length of output: 4716
🏁 Script executed:
fd -t f -e ts -e tsx -e js -e json | xargs rg -l "sessions/activity|online" | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 255
🏁 Script executed:
cat -n frontend/app/api/sessions/activity/route.tsRepository: DevLoversTeam/devlovers.net
Length of output: 2324
Use an explicit null check instead of falsy check to support displaying "0 online."
The API returns 0 when there are no active sessions (line 52 of route.ts), but if (!online) hides the popup for this valid value. Change to if (online === null) return null; if you want to display "0 online" with appropriate copy/emoji.
🤖 Prompt for AI Agents
In `@frontend/components/shared/OnlineCounterPopup.tsx` at line 24, The component
currently hides the popup for falsy online counts; update OnlineCounterPopup.tsx
to use an explicit null check on the `online` prop/variable (replace `if
(!online) return null;` with `if (online === null) return null;`) so that a
valid value of 0 is rendered (and adjust the displayed copy/emoji in the render
path to correctly show "0 online" where appropriate).
Description
This PR adds a real-time online users counter that displays at the bottom-right of the homepage. The counter shows how many users are currently active on the platform with motivational messages that encourage learning and preparation for interviews. The implementation uses PostgreSQL session tracking with automatic cleanup and includes smooth animations with theme adaptation.
Related Issue
Issue: N/A (New feature)
Changes
/api/sessions/activityPOST endpoint for session trackingactive_sessionstableOnlineCounterPopupcomponent with animated gradient designSummary by CodeRabbit
New Features
Style
✏️ Tip: You can customize this high-level summary in your review settings.