🖼️ 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.
This commit is contained in:
t0ng7u
2025-07-20 18:54:17 +08:00
parent 8bc6ddbca8
commit fd7a4461cc

View File

@@ -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