🖼️ 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:
@@ -60,6 +60,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
|
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [logoLoaded, setLogoLoaded] = useState(false);
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
const [currentLang, setCurrentLang] = useState(i18n.language);
|
const [currentLang, setCurrentLang] = useState(i18n.language);
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
@@ -226,6 +227,14 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||||||
}
|
}
|
||||||
}, [statusState?.status]);
|
}, [statusState?.status]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLogoLoaded(false);
|
||||||
|
if (!logo) return;
|
||||||
|
const img = new Image();
|
||||||
|
img.src = logo;
|
||||||
|
img.onload = () => setLogoLoaded(true);
|
||||||
|
}, [logo]);
|
||||||
|
|
||||||
const handleLanguageChange = (lang) => {
|
const handleLanguageChange = (lang) => {
|
||||||
i18n.changeLanguage(lang);
|
i18n.changeLanguage(lang);
|
||||||
setMobileMenuOpen(false);
|
setMobileMenuOpen(false);
|
||||||
@@ -496,19 +505,20 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
|
<Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
|
||||||
<Skeleton
|
<div className="relative w-8 h-8 md:w-8 md:h-8">
|
||||||
loading={isLoading}
|
{(isLoading || !logoLoaded) && (
|
||||||
active
|
|
||||||
placeholder={
|
|
||||||
<Skeleton.Image
|
<Skeleton.Image
|
||||||
active
|
active
|
||||||
className="h-7 md:h-8 !rounded-full"
|
className="absolute inset-0 !rounded-full"
|
||||||
style={{ width: 32, height: 32 }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
>
|
<img
|
||||||
<img src={logo} alt="logo" className="h-7 md:h-8 transition-transform duration-300 ease-in-out group-hover:scale-105 rounded-full" />
|
src={logo}
|
||||||
</Skeleton>
|
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="hidden md:flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
|
|||||||
Reference in New Issue
Block a user