feat(mobile): hide navbar and portfolio assistant button on scroll down, reveal on scroll up

- Mobile navbar slides out of view on scroll down and reveals on scroll up
- Portfolio assistant FAB slides down on mobile scroll down and reveals on scroll up
- Keep elements visible when drawer or chat window is open
- Reset visibility on ClientRouter page transitions
This commit is contained in:
ssbg04 committed 2026-09-13 22:50:10 +08:00
1 parent 000a45f0f9
commit f24ba80cd0
2 files changed
+142 -5

No files matched your search

+57 -4
View File
@@ -26,6 +26,7 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
});
const [drawerOpen, setDrawerOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [isNavHidden, setIsNavHidden] = useState(false);
useEffect(() => {
if (currentPath) {
@@ -39,6 +40,7 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
const handleRoute = () => {
if (typeof window !== 'undefined') setActivePath(window.location.pathname);
setDrawerOpen(false);
setIsNavHidden(false);
};
document.addEventListener('astro:page-load', handleRoute);
document.addEventListener('astro:after-swap', handleRoute);
@@ -51,13 +53,61 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
}, []);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
let lastY = typeof window !== 'undefined' ? window.scrollY : 0;
let ticking = false;
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(() => {
const currentY = window.scrollY;
// Always show at top of page or negative iOS bounce
if (currentY <= 20) {
setScrolled(false);
setIsNavHidden(false);
lastY = currentY;
ticking = false;
return;
}
setScrolled(currentY > 8);
// If drawer is open, keep navbar visible
if (drawerOpen) {
setIsNavHidden(false);
lastY = currentY;
ticking = false;
return;
}
const diff = currentY - lastY;
// Scrolling down: hide when moved down past header
if (diff > 8 && currentY > 60) {
setIsNavHidden(true);
} else if (diff < -8) {
// Scrolling up: reveal navbar
setIsNavHidden(false);
}
lastY = currentY;
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
}, [drawerOpen]);
useEffect(() => {
const onResize = () => { if (window.innerWidth >= 768) setDrawerOpen(false); };
const onResize = () => {
if (window.innerWidth >= 768) {
setDrawerOpen(false);
setIsNavHidden(false);
}
};
window.addEventListener('resize', onResize, { passive: true });
return () => window.removeEventListener('resize', onResize);
}, []);
@@ -205,10 +255,13 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
className="md:hidden fixed top-0 inset-x-0 z-50 glass-nav touch-manipulation"
style={{
height: 'var(--nav-height)',
transform: isNavHidden && !drawerOpen ? 'translateY(-100%)' : 'translateY(0)',
boxShadow: scrolled || drawerOpen
? '0 4px 20px -4px rgba(0,0,0,0.18)'
: '0 1px 0 0 var(--glass-border)',
transition: 'box-shadow 0.3s ease',
transition: 'transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s ease',
willChange: 'transform',
pointerEvents: isNavHidden && !drawerOpen ? 'none' : 'auto',
}}
>
{/* Top accent line */}
+85 -1
View File
@@ -119,9 +119,84 @@ export default function PortfolioAssistantFAB() {
const [loading, setLoading] = useState(false);
const [cooldown, setCooldown] = useState(0);
const [spamNotice, setSpamNotice] = useState<string | null>(null);
const [isBtnHidden, setIsBtnHidden] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
// Hide on scroll down, show on scroll up in mobile view
useEffect(() => {
let lastY = typeof window !== 'undefined' ? window.scrollY : 0;
let ticking = false;
const onScroll = () => {
if (window.innerWidth >= 768) {
setIsBtnHidden(false);
return;
}
if (!ticking) {
window.requestAnimationFrame(() => {
const currentY = window.scrollY;
// Always show at top of page or negative bounce
if (currentY <= 20) {
setIsBtnHidden(false);
lastY = currentY;
ticking = false;
return;
}
// Keep button visible while chat is open
if (isOpen) {
setIsBtnHidden(false);
lastY = currentY;
ticking = false;
return;
}
const diff = currentY - lastY;
// Scrolling down: hide
if (diff > 8 && currentY > 60) {
setIsBtnHidden(true);
} else if (diff < -8) {
// Scrolling up: reveal
setIsBtnHidden(false);
}
lastY = currentY;
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, [isOpen]);
useEffect(() => {
const onResize = () => {
if (window.innerWidth >= 768) {
setIsBtnHidden(false);
}
};
window.addEventListener('resize', onResize, { passive: true });
return () => window.removeEventListener('resize', onResize);
}, []);
useEffect(() => {
const handleRoute = () => {
setIsBtnHidden(false);
};
document.addEventListener('astro:page-load', handleRoute);
document.addEventListener('astro:after-swap', handleRoute);
return () => {
document.removeEventListener('astro:page-load', handleRoute);
document.removeEventListener('astro:after-swap', handleRoute);
};
}, []);
// Anti-spam countdown timer for pre-selections
useEffect(() => {
if (cooldown <= 0) return;
@@ -250,7 +325,16 @@ export default function PortfolioAssistantFAB() {
return (
<>
{/* ─── Floating Action Button (FAB) ─── */}
<div className="fixed bottom-4 sm:bottom-6 right-4 sm:right-7 z-40">
<div
className="fixed bottom-4 sm:bottom-6 right-4 sm:right-7 z-40"
style={{
transform: isBtnHidden && !isOpen ? 'translateY(calc(100% + 2rem))' : 'translateY(0)',
opacity: isBtnHidden && !isOpen ? 0 : 1,
pointerEvents: isBtnHidden && !isOpen ? 'none' : 'auto',
transition: 'transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease',
willChange: 'transform, opacity',
}}
>
<button
onClick={() => {
haptic.tap();