Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/components/header/AppMobileMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export function AppMobileMenu({
{open && (
<>
<div
className={`fixed inset-x-0 top-16 bottom-0 z-40 bg-black/50 backdrop-blur-sm transition-opacity duration-200 lg:hidden ${
className={`fixed inset-x-0 top-16 bottom-0 z-40 bg-black/50 backdrop-blur-sm transition-opacity duration-200 min-[1050px]:hidden ${
isAnimating ? 'opacity-100' : 'opacity-0'
}`}
onClick={close}
Expand All @@ -169,7 +169,7 @@ export function AppMobileMenu({

<nav
id="app-mobile-nav"
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 ${
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 ${
isAnimating ? 'translate-y-0' : '-translate-y-4'
}`}
style={{
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/header/DesktopActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function DesktopActions({
const isBlog = variant === 'blog';

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

<LanguageSwitcher />
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/header/DesktopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function DesktopNav({ variant, blogCategories = [] }: DesktopNavProps) {
};

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

if (variant === 'blog') {
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/header/MobileActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function MobileActions({
const isBlog = variant === 'blog';

return (
<div className="flex items-center gap-1 lg:hidden">
<div className="flex items-center gap-1 min-[1050px]:hidden">
<LanguageSwitcher />
{isBlog && <BlogHeaderSearch />}
{isShop && <CartButton />}
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/header/UnifiedHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function HeaderContent({
<Logo href={brandHref} />

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

{isPending && (
<div className="bg-background/95 fixed top-[65px] right-0 bottom-0 left-0 z-[60] flex items-center justify-center backdrop-blur-md">
<div className="bg-background/95 fixed top-[67px] right-0 bottom-0 left-0 z-[60] flex items-center justify-center backdrop-blur-md">
<Loader size={120} />
</div>
)}
Expand Down
6 changes: 5 additions & 1 deletion frontend/components/home/WelcomeHeroSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import React from 'react';
import { InteractiveConstellation } from '@/components/home/InteractiveConstellation';
import { InteractiveCTAButton } from '@/components/home/InteractiveCTAButton';
import { WelcomeHeroBackground } from '@/components/home/WelcomeHeroBackground';
import { OnlineCounterPopup } from '@/components/shared/OnlineCounterPopup';

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

return (
<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">
Expand Down Expand Up @@ -40,10 +42,12 @@ export default function WelcomeHeroSection() {
</p>

<div className="mt-6 sm:mt-8 md:mt-14 lg:mt-16">
<InteractiveCTAButton />
<InteractiveCTAButton ref={ctaRef} />
</div>
</div>

<OnlineCounterPopup ctaRef={ctaRef} />

<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
Expand Down
20 changes: 11 additions & 9 deletions frontend/components/shared/AnimatedNavLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ export function AnimatedNavLink({

<span
className={`absolute inset-x-0 -bottom-3 h-12 transition-opacity duration-300 ease-out ${
isActive ? 'opacity-50' : 'opacity-0 group-hover:opacity-40'
isActive
? 'opacity-25 dark:opacity-50'
: 'opacity-0 group-hover:opacity-20 dark:group-hover:opacity-40'
}`}
style={{
background: `radial-gradient(ellipse 80px 40px at center bottom, ${
Expand All @@ -62,35 +64,35 @@ export function AnimatedNavLink({
style={{
width: 0,
height: 0,
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderBottom: '8px solid var(--accent-primary)',
filter: 'drop-shadow(0 0 6px var(--accent-primary))',
borderLeft: '6px solid transparent',
borderRight: '6px solid transparent',
borderBottom: '6px solid var(--accent-primary)',
filter: 'drop-shadow(0 0 3px var(--accent-primary))',
}}
aria-hidden="true"
/>
)}

{isActive ? (
<span
className="absolute bottom-[-15px] left-1/2 h-[2px] w-full -translate-x-1/2 opacity-100 transition-all duration-300 ease-out"
className="absolute bottom-[-13px] left-1/2 h-[1.5px] w-full -translate-x-1/2 opacity-100 transition-all duration-300 ease-out"
style={{
background:
'linear-gradient(90deg, transparent 0%, var(--accent-primary) 50%, transparent 100%)',
boxShadow:
'0 0 12px 2px var(--accent-primary), 0 0 6px 1px var(--accent-primary)',
'0 0 6px 1px var(--accent-primary), 0 0 3px 0.5px var(--accent-primary)',
}}
aria-hidden="true"
/>
) : (
<span
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"
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"
style={{
background:
'linear-gradient(90deg, transparent 0%, var(--accent-hover) 20%, transparent 40%, var(--accent-hover) 60%, transparent 80%, var(--accent-hover) 100%)',
backgroundSize: '200% 100%',
animation: 'shimmer 2s ease-in-out infinite',
boxShadow: '0 0 10px 2px var(--accent-hover)',
boxShadow: '0 0 5px 1px var(--accent-hover)',
}}
aria-hidden="true"
/>
Expand Down
115 changes: 60 additions & 55 deletions frontend/components/shared/OnlineCounterPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@

import { Users } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useEffect, useLayoutEffect, useState } from 'react';
import {
useCallback,
useEffect,
useRef,
useState,
useSyncExternalStore,
} from 'react';

const SHOW_DURATION_MS = 10_000;
const SESSION_KEY = 'onlineCounterShown';

type OnlineCounterPopupProps = {
ctaRef: React.RefObject<HTMLAnchorElement | null>;
Expand All @@ -12,71 +21,67 @@ export function OnlineCounterPopup({ ctaRef }: OnlineCounterPopupProps) {
const t = useTranslations('onlineCounter');
const [online, setOnline] = useState<number | null>(null);
const [show, setShow] = useState(false);
const [position, setPosition] = useState<{ top: number; isMobile: boolean }>({
top: 0,
isMobile: true,
});

useEffect(() => {
if (sessionStorage.getItem('shown')) return;
const hideTimerRef = useRef<ReturnType<typeof setTimeout>>(null);

const fetchActivity = useCallback(() => {
fetch('/api/sessions/activity', { method: 'POST' })
.then(r => r.json())
.then(r => {
if (!r.ok) throw new Error(r.statusText);
return r.json();
})
.then(data => {
setOnline(data.online);
setTimeout(() => setShow(true), 500);
sessionStorage.setItem('shown', '1');
setTimeout(() => setShow(false), 10000);
if (typeof data.online === 'number') setOnline(data.online);
})
.catch(() => setOnline(null));
.catch(() => {});
}, []);

useLayoutEffect(() => {
const calculatePosition = () => {
const mobile = window.innerWidth < 768;
let newTop = 0;

if (mobile && ctaRef.current) {
const rect = ctaRef.current.getBoundingClientRect();
const desired = rect.bottom + window.scrollY + rect.height + 14;
const popupHeight = 56;
const safeBottom = 16;
const max =
window.scrollY + window.innerHeight - popupHeight - safeBottom;
newTop = Math.min(desired, max);
}

setPosition({ top: newTop, isMobile: mobile });
};

calculatePosition();
}, [ctaRef]);
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth < 768;
let newTop = 0;
const alreadyShown = sessionStorage.getItem(SESSION_KEY);

fetchActivity();

if (mobile && ctaRef.current) {
const rect = ctaRef.current.getBoundingClientRect();
const desired = rect.bottom + window.scrollY + rect.height + 14;
const popupHeight = 56;
const safeBottom = 16;
const max =
window.scrollY + window.innerHeight - popupHeight - safeBottom;
newTop = Math.min(desired, max);
}
if (!alreadyShown) {
const showTimer = setTimeout(() => setShow(true), 500);
sessionStorage.setItem(SESSION_KEY, '1');

setPosition({ top: newTop, isMobile: mobile });
};
hideTimerRef.current = setTimeout(
() => setShow(false),
SHOW_DURATION_MS + 500
);

window.addEventListener('resize', handleResize);
return () => {
clearTimeout(showTimer);
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
};
}
}, [fetchActivity]);

return () => {
window.removeEventListener('resize', handleResize);
};
}, [ctaRef]);
const subscribe = useCallback((cb: () => void) => {
window.addEventListener('resize', cb);
return () => window.removeEventListener('resize', cb);
}, []);

const isMobile = useSyncExternalStore(
subscribe,
() => window.innerWidth < 768,
() => true
);

const top = useSyncExternalStore(
subscribe,
() => {
if (!isMobile || !ctaRef.current) return 0;
const rect = ctaRef.current.getBoundingClientRect();
const desired = rect.bottom + rect.height + 14;
const popupHeight = 56;
const safeBottom = 16;
const max = window.innerHeight - popupHeight - safeBottom;
return Math.min(desired, max);
},
() => 0
);

if (!online) return null;
if (online === null) return null;

const getText = (count: number) => {
if (count === 1) return t('one');
Expand All @@ -89,7 +94,7 @@ export function OnlineCounterPopup({ ctaRef }: OnlineCounterPopupProps) {
return (
<div
className="fixed right-0 left-0 z-50 flex justify-center md:right-12 md:bottom-[10vh] md:left-auto md:justify-end"
style={position.isMobile ? { top: position.top } : undefined}
style={isMobile ? { top } : undefined}
>
<div
className={`transition-all duration-500 ease-out ${
Expand Down