Skip to content

Commit 13d5f77

Browse files
authored
(SP: 1) [Frontend] Integrate online users counter popup and fix header (#331)
* feat(home): add online users counter + fix header breakpoint * deleted scrollY in OnlineCounterPopup * fixed fetch in OnlineCounterPopup
1 parent 1134589 commit 13d5f77

8 files changed

Lines changed: 83 additions & 72 deletions

File tree

frontend/components/header/AppMobileMenu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export function AppMobileMenu({
160160
{open && (
161161
<>
162162
<div
163-
className={`fixed inset-x-0 top-16 bottom-0 z-40 bg-black/50 backdrop-blur-sm transition-opacity duration-200 lg:hidden ${
163+
className={`fixed inset-x-0 top-16 bottom-0 z-40 bg-black/50 backdrop-blur-sm transition-opacity duration-200 min-[1050px]:hidden ${
164164
isAnimating ? 'opacity-100' : 'opacity-0'
165165
}`}
166166
onClick={close}
@@ -169,7 +169,7 @@ export function AppMobileMenu({
169169

170170
<nav
171171
id="app-mobile-nav"
172-
className={`bg-background fixed top-16 right-0 left-0 z-50 h-[calc(100dvh-4rem)] overflow-y-auto overscroll-contain px-4 py-4 transition-transform duration-300 ease-out sm:px-6 lg:hidden lg:px-8 ${
172+
className={`bg-background fixed top-16 right-0 left-0 z-50 h-[calc(100dvh-4rem)] overflow-y-auto overscroll-contain px-4 py-4 transition-transform duration-300 ease-out sm:px-6 min-[1050px]:hidden ${
173173
isAnimating ? 'translate-y-0' : '-translate-y-4'
174174
}`}
175175
style={{

frontend/components/header/DesktopActions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function DesktopActions({
2626
const isBlog = variant === 'blog';
2727

2828
return (
29-
<div className="hidden items-center gap-2 lg:flex">
29+
<div className="hidden items-center gap-2 min-[1050px]:flex">
3030
{isBlog && <BlogHeaderSearch />}
3131

3232
<LanguageSwitcher />

frontend/components/header/DesktopNav.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function DesktopNav({ variant, blogCategories = [] }: DesktopNavProps) {
3131
};
3232

3333
if (variant === 'shop') {
34-
return <NavLinks className="lg:flex" includeHomeLink />;
34+
return <NavLinks className="min-[1050px]:flex" includeHomeLink />;
3535
}
3636

3737
if (variant === 'blog') {

frontend/components/header/MobileActions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function MobileActions({
2727
const isBlog = variant === 'blog';
2828

2929
return (
30-
<div className="flex items-center gap-1 lg:hidden">
30+
<div className="flex items-center gap-1 min-[1050px]:hidden">
3131
<LanguageSwitcher />
3232
{isBlog && <BlogHeaderSearch />}
3333
{isShop && <CartButton />}

frontend/components/header/UnifiedHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function HeaderContent({
3535
<Logo href={brandHref} />
3636

3737
<nav
38-
className="hidden items-center justify-center lg:flex"
38+
className="hidden items-center justify-center min-[1050px]:flex"
3939
aria-label="Primary"
4040
>
4141
<DesktopNav variant={variant} blogCategories={blogCategories} />
@@ -60,7 +60,7 @@ function HeaderContent({
6060
</header>
6161

6262
{isPending && (
63-
<div className="bg-background/95 fixed top-[65px] right-0 bottom-0 left-0 z-[60] flex items-center justify-center backdrop-blur-md">
63+
<div className="bg-background/95 fixed top-[67px] right-0 bottom-0 left-0 z-[60] flex items-center justify-center backdrop-blur-md">
6464
<Loader size={120} />
6565
</div>
6666
)}

frontend/components/home/WelcomeHeroSection.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import React from 'react';
88
import { InteractiveConstellation } from '@/components/home/InteractiveConstellation';
99
import { InteractiveCTAButton } from '@/components/home/InteractiveCTAButton';
1010
import { WelcomeHeroBackground } from '@/components/home/WelcomeHeroBackground';
11+
import { OnlineCounterPopup } from '@/components/shared/OnlineCounterPopup';
1112

1213
export default function WelcomeHeroSection() {
1314
const t = useTranslations('homepage');
15+
const ctaRef = React.useRef<HTMLAnchorElement>(null);
1416

1517
return (
1618
<section className="relative flex min-h-[calc(100dvh-4rem)] flex-col items-center justify-center overflow-hidden px-4 md:px-8 lg:px-12">
@@ -40,10 +42,12 @@ export default function WelcomeHeroSection() {
4042
</p>
4143

4244
<div className="mt-6 sm:mt-8 md:mt-14 lg:mt-16">
43-
<InteractiveCTAButton />
45+
<InteractiveCTAButton ref={ctaRef} />
4446
</div>
4547
</div>
4648

49+
<OnlineCounterPopup ctaRef={ctaRef} />
50+
4751
<motion.div
4852
initial={{ opacity: 0, y: 20 }}
4953
animate={{ opacity: 1, y: 0 }}

frontend/components/shared/AnimatedNavLink.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ export function AnimatedNavLink({
4545

4646
<span
4747
className={`absolute inset-x-0 -bottom-3 h-12 transition-opacity duration-300 ease-out ${
48-
isActive ? 'opacity-50' : 'opacity-0 group-hover:opacity-40'
48+
isActive
49+
? 'opacity-25 dark:opacity-50'
50+
: 'opacity-0 group-hover:opacity-20 dark:group-hover:opacity-40'
4951
}`}
5052
style={{
5153
background: `radial-gradient(ellipse 80px 40px at center bottom, ${
@@ -62,35 +64,35 @@ export function AnimatedNavLink({
6264
style={{
6365
width: 0,
6466
height: 0,
65-
borderLeft: '8px solid transparent',
66-
borderRight: '8px solid transparent',
67-
borderBottom: '8px solid var(--accent-primary)',
68-
filter: 'drop-shadow(0 0 6px var(--accent-primary))',
67+
borderLeft: '6px solid transparent',
68+
borderRight: '6px solid transparent',
69+
borderBottom: '6px solid var(--accent-primary)',
70+
filter: 'drop-shadow(0 0 3px var(--accent-primary))',
6971
}}
7072
aria-hidden="true"
7173
/>
7274
)}
7375

7476
{isActive ? (
7577
<span
76-
className="absolute bottom-[-15px] left-1/2 h-[2px] w-full -translate-x-1/2 opacity-100 transition-all duration-300 ease-out"
78+
className="absolute bottom-[-13px] left-1/2 h-[1.5px] w-full -translate-x-1/2 opacity-100 transition-all duration-300 ease-out"
7779
style={{
7880
background:
7981
'linear-gradient(90deg, transparent 0%, var(--accent-primary) 50%, transparent 100%)',
8082
boxShadow:
81-
'0 0 12px 2px var(--accent-primary), 0 0 6px 1px var(--accent-primary)',
83+
'0 0 6px 1px var(--accent-primary), 0 0 3px 0.5px var(--accent-primary)',
8284
}}
8385
aria-hidden="true"
8486
/>
8587
) : (
8688
<span
87-
className="absolute bottom-[-15px] left-1/2 h-[3px] w-0 -translate-x-1/2 opacity-0 transition-all duration-300 ease-out group-hover:w-full group-hover:opacity-100"
89+
className="absolute bottom-[-13px] left-1/2 h-[2px] w-0 -translate-x-1/2 opacity-0 transition-all duration-300 ease-out group-hover:w-full group-hover:opacity-100"
8890
style={{
8991
background:
9092
'linear-gradient(90deg, transparent 0%, var(--accent-hover) 20%, transparent 40%, var(--accent-hover) 60%, transparent 80%, var(--accent-hover) 100%)',
9193
backgroundSize: '200% 100%',
9294
animation: 'shimmer 2s ease-in-out infinite',
93-
boxShadow: '0 0 10px 2px var(--accent-hover)',
95+
boxShadow: '0 0 5px 1px var(--accent-hover)',
9496
}}
9597
aria-hidden="true"
9698
/>

frontend/components/shared/OnlineCounterPopup.tsx

Lines changed: 60 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22

33
import { Users } from 'lucide-react';
44
import { 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

716
type 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

Comments
 (0)