From fd7a4461cc33d2ad12c527debad0ecf0446f0f47 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sun, 20 Jul 2025 18:54:17 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F=20feat(header):=20improve?= =?UTF-8?q?=20logo=20loading=20UX=20with=20skeleton=20overlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure the header logo is shown only after the image has fully loaded to eliminate flicker: • Introduced `logoLoaded` state to track image load completion. • Pre-loaded the logo using `new Image()` inside a `useEffect` hook and set state on `onload`. • Replaced the previous Skeleton wrapper with a stacked layout: – A `Skeleton.Image` placeholder is rendered while the logo is loading. – The real `` element fades in with an opacity transition once both global `isLoading` and `logoLoaded` are true. • Added automatic reset of `logoLoaded` whenever the logo source changes. • Removed redundant `onLoad` on the `` tag to avoid double triggers. • Ensured placeholder and image sizes match via absolute positioning to prevent layout shift. This delivers a smoother visual experience by keeping the skeleton visible until the logo is completely ready and then revealing it seamlessly. --- web/src/components/layout/HeaderBar.js | 30 +++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/web/src/components/layout/HeaderBar.js b/web/src/components/layout/HeaderBar.js index a097f79c..a2e3986c 100644 --- a/web/src/components/layout/HeaderBar.js +++ b/web/src/components/layout/HeaderBar.js @@ -60,6 +60,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { const isMobile = useIsMobile(); const [collapsed, toggleCollapsed] = useSidebarCollapsed(); const [isLoading, setIsLoading] = useState(true); + const [logoLoaded, setLogoLoaded] = useState(false); let navigate = useNavigate(); const [currentLang, setCurrentLang] = useState(i18n.language); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); @@ -226,6 +227,14 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { } }, [statusState?.status]); + useEffect(() => { + setLogoLoaded(false); + if (!logo) return; + const img = new Image(); + img.src = logo; + img.onload = () => setLogoLoaded(true); + }, [logo]); + const handleLanguageChange = (lang) => { i18n.changeLanguage(lang); setMobileMenuOpen(false); @@ -496,19 +505,20 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { /> handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2"> - + {(isLoading || !logoLoaded) && ( - } - > - logo - + )} + logo +