22
33import { Users } from 'lucide-react' ;
44import { useTranslations } from 'next-intl' ;
5- import { useEffect , useLayoutEffect , useState } from 'react' ;
5+ import {
6+ useCallback ,
7+ useEffect ,
8+ useRef ,
9+ useState ,
10+ useSyncExternalStore ,
11+ } from 'react' ;
12+
13+ const SHOW_DURATION_MS = 10_000 ;
14+ const SESSION_KEY = 'onlineCounterShown' ;
615
716type OnlineCounterPopupProps = {
817 ctaRef : React . RefObject < HTMLAnchorElement | null > ;
@@ -12,71 +21,67 @@ export function OnlineCounterPopup({ ctaRef }: OnlineCounterPopupProps) {
1221 const t = useTranslations ( 'onlineCounter' ) ;
1322 const [ online , setOnline ] = useState < number | null > ( null ) ;
1423 const [ show , setShow ] = useState ( false ) ;
15- const [ position , setPosition ] = useState < { top : number ; isMobile : boolean } > ( {
16- top : 0 ,
17- isMobile : true ,
18- } ) ;
19-
20- useEffect ( ( ) => {
21- if ( sessionStorage . getItem ( 'shown' ) ) return ;
24+ const hideTimerRef = useRef < ReturnType < typeof setTimeout > > ( null ) ;
2225
26+ const fetchActivity = useCallback ( ( ) => {
2327 fetch ( '/api/sessions/activity' , { method : 'POST' } )
24- . then ( r => r . json ( ) )
28+ . then ( r => {
29+ if ( ! r . ok ) throw new Error ( r . statusText ) ;
30+ return r . json ( ) ;
31+ } )
2532 . then ( data => {
26- setOnline ( data . online ) ;
27- setTimeout ( ( ) => setShow ( true ) , 500 ) ;
28- sessionStorage . setItem ( 'shown' , '1' ) ;
29- setTimeout ( ( ) => setShow ( false ) , 10000 ) ;
33+ if ( typeof data . online === 'number' ) setOnline ( data . online ) ;
3034 } )
31- . catch ( ( ) => setOnline ( null ) ) ;
35+ . catch ( ( ) => { } ) ;
3236 } , [ ] ) ;
3337
34- useLayoutEffect ( ( ) => {
35- const calculatePosition = ( ) => {
36- const mobile = window . innerWidth < 768 ;
37- let newTop = 0 ;
38-
39- if ( mobile && ctaRef . current ) {
40- const rect = ctaRef . current . getBoundingClientRect ( ) ;
41- const desired = rect . bottom + window . scrollY + rect . height + 14 ;
42- const popupHeight = 56 ;
43- const safeBottom = 16 ;
44- const max =
45- window . scrollY + window . innerHeight - popupHeight - safeBottom ;
46- newTop = Math . min ( desired , max ) ;
47- }
48-
49- setPosition ( { top : newTop , isMobile : mobile } ) ;
50- } ;
51-
52- calculatePosition ( ) ;
53- } , [ ctaRef ] ) ;
5438 useEffect ( ( ) => {
55- const handleResize = ( ) => {
56- const mobile = window . innerWidth < 768 ;
57- let newTop = 0 ;
39+ const alreadyShown = sessionStorage . getItem ( SESSION_KEY ) ;
40+
41+ fetchActivity ( ) ;
5842
59- if ( mobile && ctaRef . current ) {
60- const rect = ctaRef . current . getBoundingClientRect ( ) ;
61- const desired = rect . bottom + window . scrollY + rect . height + 14 ;
62- const popupHeight = 56 ;
63- const safeBottom = 16 ;
64- const max =
65- window . scrollY + window . innerHeight - popupHeight - safeBottom ;
66- newTop = Math . min ( desired , max ) ;
67- }
43+ if ( ! alreadyShown ) {
44+ const showTimer = setTimeout ( ( ) => setShow ( true ) , 500 ) ;
45+ sessionStorage . setItem ( SESSION_KEY , '1' ) ;
6846
69- setPosition ( { top : newTop , isMobile : mobile } ) ;
70- } ;
47+ hideTimerRef . current = setTimeout (
48+ ( ) => setShow ( false ) ,
49+ SHOW_DURATION_MS + 500
50+ ) ;
7151
72- window . addEventListener ( 'resize' , handleResize ) ;
52+ return ( ) => {
53+ clearTimeout ( showTimer ) ;
54+ if ( hideTimerRef . current ) clearTimeout ( hideTimerRef . current ) ;
55+ } ;
56+ }
57+ } , [ fetchActivity ] ) ;
7358
74- return ( ) => {
75- window . removeEventListener ( 'resize' , handleResize ) ;
76- } ;
77- } , [ ctaRef ] ) ;
59+ const subscribe = useCallback ( ( cb : ( ) => void ) => {
60+ window . addEventListener ( 'resize' , cb ) ;
61+ return ( ) => window . removeEventListener ( 'resize' , cb ) ;
62+ } , [ ] ) ;
63+
64+ const isMobile = useSyncExternalStore (
65+ subscribe ,
66+ ( ) => window . innerWidth < 768 ,
67+ ( ) => true
68+ ) ;
69+
70+ const top = useSyncExternalStore (
71+ subscribe ,
72+ ( ) => {
73+ if ( ! isMobile || ! ctaRef . current ) return 0 ;
74+ const rect = ctaRef . current . getBoundingClientRect ( ) ;
75+ const desired = rect . bottom + rect . height + 14 ;
76+ const popupHeight = 56 ;
77+ const safeBottom = 16 ;
78+ const max = window . innerHeight - popupHeight - safeBottom ;
79+ return Math . min ( desired , max ) ;
80+ } ,
81+ ( ) => 0
82+ ) ;
7883
79- if ( ! online ) return null ;
84+ if ( online === null ) return null ;
8085
8186 const getText = ( count : number ) => {
8287 if ( count === 1 ) return t ( 'one' ) ;
@@ -89,7 +94,7 @@ export function OnlineCounterPopup({ ctaRef }: OnlineCounterPopupProps) {
8994 return (
9095 < div
9196 className = "fixed right-0 left-0 z-50 flex justify-center md:right-12 md:bottom-[10vh] md:left-auto md:justify-end"
92- style = { position . isMobile ? { top : position . top } : undefined }
97+ style = { isMobile ? { top } : undefined }
9398 >
9499 < div
95100 className = { `transition-all duration-500 ease-out ${
0 commit comments