feat(images): add lazy loading for images

This commit is contained in:
2025-02-04 19:18:32 +01:00
parent e1a670cf6d
commit 8e7e3457b0
10 changed files with 295 additions and 151 deletions

38
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.4.0",
"react-lazy-load-image-component": "^1.6.3",
"react-router-dom": "^7.0.2"
},
"devDependencies": {
@@ -24,6 +25,7 @@
"@simonsmith/cypress-image-snapshot": "^9.1.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-lazy-load-image-component": "^1.6.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.18.0",
"eslint-plugin-react-hooks": "^5.1.0",
@@ -1864,6 +1866,17 @@
"@types/react": "*"
}
},
"node_modules/@types/react-lazy-load-image-component": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/@types/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.4.tgz",
"integrity": "sha512-8pFPeDPF4yVG4lU1/ixZidJEEDZmEOYOTYDvmIu2IAabyuv97Q7n/93nMCocHvQ7vD1czKGiW+op55D9m3MkdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
@@ -5603,6 +5616,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -5615,6 +5634,12 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
"license": "MIT"
},
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -6495,6 +6520,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/react-lazy-load-image-component": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.3.tgz",
"integrity": "sha512-kdQYUDbuISF3T9El0sBLNoWrmPohqlytcG4ognLtHYjY8bZAsJ0/Ez+VaV+0QlVyUY3K6dDXkuQAz3GpvdjBkw==",
"license": "MIT",
"dependencies": {
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1"
},
"peerDependencies": {
"react": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x || ^19.x.x"
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",

View File

@@ -34,6 +34,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.4.0",
"react-lazy-load-image-component": "^1.6.3",
"react-router-dom": "^7.0.2"
},
"devDependencies": {
@@ -41,6 +42,7 @@
"@simonsmith/cypress-image-snapshot": "^9.1.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-lazy-load-image-component": "^1.6.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.18.0",
"eslint-plugin-react-hooks": "^5.1.0",

View File

@@ -1,21 +1,31 @@
import './banner.css';
import { LazyLoadImage } from 'react-lazy-load-image-component';
interface BannerProps {
image: { src: string, alt?: string };
header: string;
children?: React.ReactNode;
label?: string;
image: { src: string; alt?: string };
header: string;
children?: React.ReactNode;
label?: string;
}
export const Banner: React.FC<BannerProps> = ({ image, header, children, label }) => {
return (
<div className={'banner'}>
<img className={'banner-image'} src={image.src} alt={image.alt} />
<div className={'banner-content'}>
<h3 className={'banner-header'}>{header}</h3>
<p className={'banner-text'}>{children}</p>
{ label && <span className={'banner-label'}>{label}</span> }
</div>
</div>
);
}
export const Banner: React.FC<BannerProps> = ({
image,
header,
children,
label,
}) => {
return (
<div className={'banner'}>
<LazyLoadImage
className={'banner-image'}
src={image.src}
alt={image.alt}
/>
<div className={'banner-content'}>
<h3 className={'banner-header'}>{header}</h3>
<p className={'banner-text'}>{children}</p>
{label && <span className={'banner-label'}>{label}</span>}
</div>
</div>
);
};

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { NavArrowLeft, NavArrowRight } from 'iconoir-react';
import './carousel.css';
import { LazyLoadImage } from 'react-lazy-load-image-component';
interface CarouselProps {
carousel: CarouselImageProps[];
@@ -78,7 +79,7 @@ export const Carousel: React.FC<CarouselProps> = ({ carousel }) => {
<div className={'carousel-slider'} ref={carouselSlider}>
{[...carousel, ...carousel].map((image, index) => (
<div key={index} className={'slide'}>
<img
<LazyLoadImage
key={index}
className={`slide-image ${index === currentIndex ? 'active' : ''}`}
src={image.image}

View File

@@ -1,33 +1,47 @@
interface PartnerProps {
name: string;
logo: string;
texts: React.ReactNode[];
photos: string[];
link: string;
name: string;
logo: string;
texts: React.ReactNode[];
photos: string[];
link: string;
}
import { ArrowUpRight } from 'iconoir-react';
import { LazyLoadImage } from 'react-lazy-load-image-component';
import './partner.css';
export const Partner: React.FC<PartnerProps> = ({ name, logo, texts, photos, link }) => (
<div className={'partner'}>
<div className={'partner-infos'}>
<img className={'partner-logo'} src={logo} alt={name} />
<div className={'partner-content'}>
<h2 className={'partner-name'}>{name}</h2>
{texts.map((text, index) => (
<p key={index} className={'partner-text'}>{text}</p>
))}
</div>
<a className={'partner-link'} target={'_blank'} href={link}>
<ArrowUpRight />
Accéder au site
</a>
</div>
<div className={'partner-photos'}>
{photos.map((photo, index) => (
<img key={index} className={'partner-preview'} src={photo} alt={`Photo ${index + 1} - ${name}`} />
))}
</div>
export const Partner: React.FC<PartnerProps> = ({
name,
logo,
texts,
photos,
link,
}) => (
<div className={'partner'}>
<div className={'partner-infos'}>
<LazyLoadImage className={'partner-logo'} src={logo} alt={name} />
<div className={'partner-content'}>
<h2 className={'partner-name'}>{name}</h2>
{texts.map((text, index) => (
<p key={index} className={'partner-text'}>
{text}
</p>
))}
</div>
<a className={'partner-link'} target={'_blank'} href={link}>
<ArrowUpRight />
Accéder au site
</a>
</div>
);
<div className={'partner-photos'}>
{photos.map((photo, index) => (
<img
key={index}
className={'partner-preview'}
src={photo}
alt={`Photo ${index + 1} - ${name}`}
/>
))}
</div>
</div>
);

View File

@@ -1,51 +1,56 @@
import React from "react";
import React from 'react';
import "./team.css";
import './team.css';
import { LazyLoadImage } from 'react-lazy-load-image-component';
interface TeamMemberProps {
name: string;
role: string;
image: string;
name: string;
role: string;
image: string;
}
interface TeamGroupProps {
title: string;
members: TeamMemberProps[];
title: string;
members: TeamMemberProps[];
}
interface TeamProps {
groups: TeamGroupProps[];
groups: TeamGroupProps[];
}
const TeamMember: React.FC<TeamMemberProps> = ({ name, role, image }) => {
return (
<div className='team-member'>
<div className='team-member-image-container'>
<img className='team-member-image' src={image} alt={name} />
</div>
<div className='team-member-content'>
<h5 className='team-member-name'>{name}</h5>
<p className='team-member-role'>{role}</p>
</div>
</div>
);
}
return (
<div className="team-member">
<div className="team-member-image-container">
<LazyLoadImage className="team-member-image" src={image} alt={name} />
</div>
<div className="team-member-content">
<h5 className="team-member-name">{name}</h5>
<p className="team-member-role">{role}</p>
</div>
</div>
);
};
const TeamGroup: React.FC<TeamGroupProps> = ({ title, members }) => {
return (
<div className='team-group'>
<h3 className='team-group-title'>{title}</h3>
<div className='team-group-list'>
{members.map((member, index) => <TeamMember key={index} {...member} />)}
</div>
</div>
);
}
return (
<div className="team-group">
<h3 className="team-group-title">{title}</h3>
<div className="team-group-list">
{members.map((member, index) => (
<TeamMember key={index} {...member} />
))}
</div>
</div>
);
};
export const Team: React.FC<TeamProps> = ({ groups }) => {
return (
<div className={"team"}>
{groups.map((group, index) => <TeamGroup key={index} {...group} />)}
</div>
);
}
return (
<div className={'team'}>
{groups.map((group, index) => (
<TeamGroup key={index} {...group} />
))}
</div>
);
};

View File

@@ -1,34 +1,37 @@
import React from "react";
import React from 'react';
import './timeline.css';
import { LazyLoadImage } from 'react-lazy-load-image-component';
interface StatCardProps {
type: "stat";
type: 'stat';
data: string;
label: string;
};
}
interface ImageCardProps {
type: "image";
type: 'image';
src: string;
alt: string;
fit: "contain" | "cover";
};
fit: 'contain' | 'cover';
}
interface TimelineProjectProps {
title: string;
date: string;
banner?: string;
paragraphs: React.ReactNode[];
cards?: (StatCardProps|ImageCardProps)[];
};
cards?: (StatCardProps | ImageCardProps)[];
}
interface TimelineProps {
projects: TimelineProjectProps[];
};
}
const TimelineProjectCard: React.FC<StatCardProps | ImageCardProps> = (props) => {
if (props.type === "stat") {
const TimelineProjectCard: React.FC<StatCardProps | ImageCardProps> = (
props
) => {
if (props.type === 'stat') {
const { data, label } = props as StatCardProps;
return (
<div className={'timeline-project-card card_stat'}>
@@ -36,46 +39,63 @@ const TimelineProjectCard: React.FC<StatCardProps | ImageCardProps> = (props) =>
<p className={'timeline-project-card-subtext'}>{label}</p>
</div>
);
} else if (props.type === "image") {
} else if (props.type === 'image') {
const { src, alt, fit } = props as ImageCardProps;
return (
<div className={`timeline-project-card card_image_${fit}`}>
<img className={"timeline-project-card-image"} src={src} alt={alt} />
<LazyLoadImage
className={'timeline-project-card-image'}
src={src}
alt={alt}
/>
</div>
);
}
};
export const TimelineProject: React.FC<TimelineProjectProps> = ({ title, banner, date, paragraphs, cards }) => {
export const TimelineProject: React.FC<TimelineProjectProps> = ({
title,
banner,
date,
paragraphs,
cards,
}) => {
return (
<div className={'timeline-project'}>
<div className={'timeline-project-content'}>
<h3 className={'timeline-project-title'}>{title}</h3>
<p className={'timeline-project-date'}>{date}</p>
</div>
{ banner && <img className={'timeline-project-banner'} src={banner} alt={title} /> }
{banner && (
<LazyLoadImage
className={'timeline-project-banner'}
src={banner}
alt={title}
/>
)}
<div className={'timeline-project-description'}>
{ paragraphs.map((paragraph, i) => <p className={'timeline-project-paragraph'} key={i}>{paragraph}</p>) }
{paragraphs.map((paragraph, i) => (
<p className={'timeline-project-paragraph'} key={i}>
{paragraph}
</p>
))}
</div>
<div className={'timeline-project-cards'}>
{ cards && cards.map((card, i) => <TimelineProjectCard key={i} {...card} />) }
{cards &&
cards.map((card, i) => <TimelineProjectCard key={i} {...card} />)}
</div>
</div>
);
}
};
export const Timeline: React.FC<TimelineProps> = ({ projects }) => {
return (
<div className={'timeline'}>
<h4 className={'timeline-tag'}>Aujourd'hui</h4>
{
projects.map((project, i) => {
return (
<TimelineProject key={i} {...project} />
);
})
}
{projects.map((project, i) => {
return <TimelineProject key={i} {...project} />;
})}
<h4 className={'timeline-tag'}>Il y a quelques temps</h4>
</div>
);
}
};

View File

@@ -2,6 +2,7 @@ import React from 'react';
import './footer.css';
import { Link } from 'react-router-dom';
import { LazyLoadImage } from 'react-lazy-load-image-component';
interface FooterLinkProps {
text: string;
@@ -9,11 +10,15 @@ interface FooterLinkProps {
target?: string;
}
const FooterLink: React.FC<FooterLinkProps> = ({ text, link, target="_self" }) => {
const FooterLink: React.FC<FooterLinkProps> = ({
text,
link,
target = '_self',
}) => {
return (
<Link to={link} className={`footer-link`} target={target}>
{text}
</Link>
<Link to={link} className={`footer-link`} target={target}>
{text}
</Link>
);
};
@@ -21,35 +26,61 @@ export const Footer: React.FC = () => {
return (
<footer className={'footer'}>
<div className={'footer-content'}>
<img className={'footer-logo'} src={'https://r2.modelec.club/logo.png'} alt={'Modelec'} />
<LazyLoadImage
className={'footer-logo'}
src={'https://r2.modelec.club/logo.png'}
alt={'Modelec'}
/>
<div className={'footer-part'}>
<h3 className={'footer-title'}>Plan du site</h3>
<div className={'footer-links'}>
{[
{ text: 'Accueil', link: '/' },
{ text: 'Projets', link: '/projets/' },
/*
/*
{ text: 'Matériels', link: '/materiels/' },
{ text: 'Photos', link: '/photos/' },
*/
{ text: 'Partenaires', link: '/partenaires/' },
{ text: 'Nous contacter', link: '/contact/' },
].map((link) => <FooterLink key={link.text} {...link} />)}
].map((link) => (
<FooterLink key={link.text} {...link} />
))}
</div>
</div>
<div className={'footer-part'}>
<h3 className={'footer-title'}>Nos réseaux</h3>
<div className={'footer-links'}>
{[
{ text: 'Instagram', link: 'https://www.instagram.com/modelec_isen', target: '_blank' },
{ text: 'Youtube', link: 'https://www.youtube.com/@Modelec-ISEN', target: '_blank' },
{ text: 'Github', link: 'https://www.github.com/modelec', target: '_blank' },
{ text: 'Mail', link: 'mailto:contact@modelec.club', target: '_blank' },
].map((link) => <FooterLink key={link.text} {...link} />)}
{
text: 'Instagram',
link: 'https://www.instagram.com/modelec_isen',
target: '_blank',
},
{
text: 'Youtube',
link: 'https://www.youtube.com/@Modelec-ISEN',
target: '_blank',
},
{
text: 'Github',
link: 'https://www.github.com/modelec',
target: '_blank',
},
{
text: 'Mail',
link: 'mailto:contact@modelec.club',
target: '_blank',
},
].map((link) => (
<FooterLink key={link.text} {...link} />
))}
</div>
</div>
</div>
<span className={'footer-credits'}>© 2024 Modelec ISEN Nantes. Site réalisé par AppenISEN.</span>
<span className={'footer-credits'}>
© 2024 Modelec ISEN Nantes. Site réalisé par AppenISEN.
</span>
</footer>
);
};

View File

@@ -5,6 +5,7 @@ import { Menu } from 'iconoir-react';
import './navbar.css';
import { useWindowsSize } from '../../hooks/useWindowsSize';
import { Link, useLocation } from 'react-router-dom';
import { LazyLoadImage } from 'react-lazy-load-image-component';
interface NavbarLinkProps {
text: string;
@@ -14,37 +15,46 @@ interface NavbarLinkProps {
const NavbarLink: React.FC<NavbarLinkProps> = ({ text, link, isActive }) => {
return (
<Link to={link} className={`navbar-link ${isActive ? "link_active" : ""}`}>
{text}
</Link>
<Link to={link} className={`navbar-link ${isActive ? 'link_active' : ''}`}>
{text}
</Link>
);
};
const MobileNavbarLink: React.FC<NavbarLinkProps> = ({ text, link, isActive }) => {
const MobileNavbarLink: React.FC<NavbarLinkProps> = ({
text,
link,
isActive,
}) => {
return (
<Link to={link} className={`mobileNavbar-link ${isActive ? "link_active" : ""}`}>
{text}
</Link>
<Link
to={link}
className={`mobileNavbar-link ${isActive ? 'link_active' : ''}`}
>
{text}
</Link>
);
};
export const Navbar = () => {
const location = useLocation();
const pathname = location.pathname.endsWith('/') ? location.pathname : location.pathname + '/';
const pathname = location.pathname.endsWith('/')
? location.pathname
: location.pathname + '/';
const links = [
{ text: 'Accueil', link: '/' },
{ text: 'Projets', link: '/projets/' },
/*
/*
{ text: 'Matériels', link: '/materiels/' },
{ text: 'Photos', link: '/photos/' },
*/
{ text: 'Partenaires', link: '/partenaires/' },
{ text: 'Nous contacter', link: '/contact/' },
]
];
const activeLink = links.findIndex((link) => link.link === pathname);
const activeLink = links.findIndex(link => link.link === pathname);
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
const { width } = useWindowsSize();
@@ -58,43 +68,55 @@ export const Navbar = () => {
<>
<nav className={'navbar'}>
<Link to={'/'} className={'navbar-logo'}>
<img
<LazyLoadImage
className={'navbar-logo-img img_large'}
src={'https://r2.modelec.club/logo-full.png'} // TODO: Change to SVG logo / import it from assets
alt={'Modelec Logo'}
/>
<img
<LazyLoadImage
className={'navbar-logo-img img_small'}
src={'https://r2.modelec.club/logo.png'} // TODO: Change to SVG logo / import it from assets
alt={'Modelec Logo'}
/>
</Link>
<div className={'navbar-links'}>
{
links.map((link, index) => (
<NavbarLink key={index} text={link.text} link={link.link} isActive={activeLink == index} />
))
}
{links.map((link, index) => (
<NavbarLink
key={index}
text={link.text}
link={link.link}
isActive={activeLink == index}
/>
))}
</div>
<button className={'navbar-open'} onClick={() => setIsMobileMenuOpen(old => !old)}>
<button
className={'navbar-open'}
onClick={() => setIsMobileMenuOpen((old) => !old)}
>
<Menu />
</button>
{ isMobileMenuOpen &&
{isMobileMenuOpen && (
<nav className={'mobileNavbar'}>
<h1 className={'mobileNavbar-title'}>Se déplacer sur le site</h1>
<div className={'mobileNavbar-links'}>
{
links.map((link, index) => (
<MobileNavbarLink key={index} text={link.text} link={link.link} isActive={activeLink == index} />
))
}
{links.map((link, index) => (
<MobileNavbarLink
key={index}
text={link.text}
link={link.link}
isActive={activeLink == index}
/>
))}
</div>
</nav>
}
)}
</nav>
{ isMobileMenuOpen &&
<div className={'mobileNavbar-overlay'} onClick={() => setIsMobileMenuOpen(false)}></div>
}
{isMobileMenuOpen && (
<div
className={'mobileNavbar-overlay'}
onClick={() => setIsMobileMenuOpen(false)}
></div>
)}
</>
);
};

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { LazyLoadImage } from 'react-lazy-load-image-component';
import { Github, Youtube, Instagram, BookmarkBook } from 'iconoir-react';
@@ -84,7 +85,7 @@ const Home: React.FC = () => {
target={'_blank'}
href={'https://isen-nantes.fr'}
>
<img
<LazyLoadImage
className={'partners-partner-logo'}
src={'https://r2.modelec.club/isen.png'}
alt={'ISEN Nantes'}
@@ -96,7 +97,7 @@ const Home: React.FC = () => {
target={'_blank'}
href={'https://www.tracopower.com/fr/fra'}
>
<img
<LazyLoadImage
className={'partners-partner-logo'}
src={'https://r2.modelec.club/tracopower.jpeg'}
alt={'Traco Power'}
@@ -108,7 +109,7 @@ const Home: React.FC = () => {
target={'_blank'}
href={'https://mercurycloud.fr'}
>
<img
<LazyLoadImage
className={'partners-partner-logo'}
src={'https://r2.modelec.club/mercurycloud.png'}
alt={'Mercury Cloud'}
@@ -120,7 +121,7 @@ const Home: React.FC = () => {
target={'_blank'}
href={'https://instagram.com/odyssey_bde'}
>
<img
<LazyLoadImage
className={'partners-partner-logo'}
src={'https://r2.modelec.club/bde.png'}
alt={'BDE Odyssey'}