Browse Source

🖼️ feat(header): improve logo loading UX with skeleton overlay

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 `<img>` 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 `<img>` 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.
t0ng7u 5 months ago
parent
commit
fd7a4461cc
1 changed files with 20 additions and 10 deletions
  1. 20 10
      web/src/components/layout/HeaderBar.js

+ 20 - 10
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 }) => {
               />
             </div>
             <Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
-              <Skeleton
-                loading={isLoading}
-                active
-                placeholder={
+              <div className="relative w-8 h-8 md:w-8 md:h-8">
+                {(isLoading || !logoLoaded) && (
                   <Skeleton.Image
                     active
-                    className="h-7 md:h-8 !rounded-full"
-                    style={{ width: 32, height: 32 }}
+                    className="absolute inset-0 !rounded-full"
+                    style={{ width: '100%', height: '100%' }}
                   />
-                }
-              >
-                <img src={logo} alt="logo" className="h-7 md:h-8 transition-transform duration-300 ease-in-out group-hover:scale-105 rounded-full" />
-              </Skeleton>
+                )}
+                <img
+                  src={logo}
+                  alt="logo"
+                  className={`absolute inset-0 w-full h-full transition-opacity duration-200 group-hover:scale-105 rounded-full ${(!isLoading && logoLoaded) ? 'opacity-100' : 'opacity-0'}`}
+                />
+              </div>
               <div className="hidden md:flex items-center gap-2">
                 <div className="flex items-center gap-2">
                   <Skeleton