-
Notifications
You must be signed in to change notification settings - Fork 108
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cow-fi): lazy load cms images /learn/ pages (#4906)
* feat: lazy load cms images /learn/ pages * feat: lazy load cms images /learn/ pages * feat: update sitemap logic and lighthouse optimisations * feat: lazy load svgs and update meta title/descriptions * feat: fix meta descriptions for /learn/ pages * feat: address comment uzeLazyload * feat: optimize learn page code * feat: lazy load cms images /learn/ pages * feat: cleanup meta description token function * feat: add conditional load of GTM * feat: address comments * fix: navitems list style css * feat: conslidate CategoryLinks component * feat: change import path * fix: mobile menu list type
- Loading branch information
1 parent
47b7115
commit 897ce91
Showing
39 changed files
with
865 additions
and
520 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from 'react' | ||
import { clickOnKnowledgeBase } from 'modules/analytics' | ||
import { LinkItem, LinkColumn } from '@/styles/styled' | ||
import { Article } from 'services/cms' | ||
|
||
interface ArticlesListProps { | ||
articles: Article[] | ||
} | ||
|
||
const ARTICLES_PATH = '/learn/articles/' | ||
|
||
export const ArticlesList: React.FC<ArticlesListProps> = ({ articles }) => ( | ||
<LinkColumn> | ||
{articles.map((article) => { | ||
if (!article.attributes) return null | ||
|
||
const { slug, title } = article.attributes | ||
|
||
return ( | ||
<LinkItem | ||
key={article.id} | ||
href={`${ARTICLES_PATH}${slug}`} | ||
onClick={() => clickOnKnowledgeBase(`click-article-${title}`)} | ||
> | ||
{title} | ||
<span>→</span> | ||
</LinkItem> | ||
) | ||
})} | ||
</LinkColumn> | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import React from 'react' | ||
import styled from 'styled-components/macro' | ||
import { clickOnKnowledgeBase } from 'modules/analytics' | ||
import { Color, Media } from '@cowprotocol/ui' | ||
interface Category { | ||
name: string | ||
slug: string | ||
} | ||
|
||
interface CategoryLinksProps { | ||
allCategories: Category[] | ||
noDivider?: boolean | ||
} | ||
|
||
const CategoryLinksWrapper = styled.ul<{ noDivider?: boolean }>` | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
gap: 32px; | ||
padding: 0; | ||
margin: 0; | ||
list-style: none; | ||
font-size: 16px; | ||
font-weight: 500; | ||
color: ${Color.neutral50}; | ||
width: 100%; | ||
scrollbar-width: thin; | ||
scrollbar-color: ${Color.neutral70} ${Color.neutral90}; | ||
-webkit-overflow-scrolling: touch; | ||
&::-webkit-scrollbar { | ||
width: 8px; | ||
} | ||
&::-webkit-scrollbar-track { | ||
background: ${Color.neutral90}; | ||
border-radius: 10px; | ||
} | ||
&::-webkit-scrollbar-thumb { | ||
background: ${Color.neutral70}; | ||
border-radius: 10px; | ||
} | ||
&::-webkit-scrollbar-thumb:hover { | ||
background: ${Color.neutral50}; | ||
} | ||
${Media.upToMedium()} { | ||
overflow-x: auto; | ||
overflow-y: hidden; | ||
flex-flow: row nowrap; | ||
justify-content: flex-start; | ||
gap: 24px; | ||
padding: 16px 34px 16px 16px; | ||
} | ||
li { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
&:first-child { | ||
margin-right: ${({ noDivider }) => (noDivider ? '0' : '-32px')}; | ||
} | ||
&:first-child::after { | ||
content: ${({ noDivider }) => (noDivider ? 'none' : "'|'")}; | ||
margin: 0 16px; | ||
display: flex; | ||
height: 100%; | ||
width: 16px; | ||
align-items: center; | ||
justify-content: center; | ||
} | ||
} | ||
a { | ||
color: ${Color.neutral40}; | ||
text-decoration: none; | ||
transition: color 0.2s ease-in-out; | ||
white-space: nowrap; | ||
line-height: 1; | ||
&:hover { | ||
color: ${Color.neutral0}; | ||
} | ||
} | ||
` | ||
|
||
export const CategoryLinks: React.FC<CategoryLinksProps> = ({ allCategories, noDivider }) => ( | ||
<CategoryLinksWrapper noDivider={noDivider}> | ||
<li> | ||
<a href="/learn" onClick={() => clickOnKnowledgeBase('click-categories-home')}> | ||
Knowledge Base | ||
</a> | ||
</li> | ||
{allCategories.map((category) => ( | ||
<li key={category.slug}> | ||
<a | ||
href={`/learn/topic/${category.slug}`} | ||
onClick={() => clickOnKnowledgeBase(`click-categories-${category.name}`)} | ||
> | ||
{category.name} | ||
</a> | ||
</li> | ||
))} | ||
</CategoryLinksWrapper> | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import React, { useEffect, useRef, useState } from 'react' | ||
import SVG, { Props as SVGProps } from 'react-inlinesvg' | ||
|
||
interface LazySVGProps extends Omit<SVGProps, 'loader'> { | ||
src: string | ||
loader?: React.ReactNode | ||
rootMargin?: string | ||
} | ||
|
||
const LazySVG: React.FC<LazySVGProps> = ({ | ||
src, | ||
loader = <div>Loading SVG...</div>, | ||
rootMargin = '100px', | ||
width, | ||
height, | ||
...props | ||
}) => { | ||
const [isInView, setIsInView] = useState(false) | ||
const wrapperRef = useRef<HTMLSpanElement>(null) | ||
|
||
useEffect(() => { | ||
if (!wrapperRef.current) { | ||
return | ||
} | ||
|
||
const observer = new IntersectionObserver( | ||
(entries) => { | ||
entries.forEach((entry) => { | ||
if (entry.isIntersecting) { | ||
setIsInView(true) | ||
observer.disconnect() | ||
} | ||
}) | ||
}, | ||
{ | ||
root: null, | ||
rootMargin: rootMargin, | ||
threshold: 0, | ||
}, | ||
) | ||
|
||
observer.observe(wrapperRef.current) | ||
|
||
return () => { | ||
observer.disconnect() | ||
} | ||
}, [rootMargin, src]) | ||
|
||
return ( | ||
<span ref={wrapperRef}> | ||
{isInView ? ( | ||
<SVG | ||
src={src} | ||
{...props} | ||
loader={loader} | ||
width={typeof width === 'number' ? width : undefined} | ||
height={typeof height === 'number' ? height : undefined} | ||
/> | ||
) : ( | ||
loader | ||
)} | ||
</span> | ||
) | ||
} | ||
|
||
export default LazySVG |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import React, { useEffect, useRef, useCallback, FC, ImgHTMLAttributes, memo } from 'react' | ||
|
||
let observer: IntersectionObserver | null = null | ||
const callbacks: ((entry: IntersectionObserverEntry) => void)[] = [] | ||
|
||
const LAZY_LOADING_CONFIG = { | ||
rootMargin: '25px', | ||
placeholderColor: '#f0f0f0', | ||
fadeInDuration: '0.3s', | ||
minHeight: '100px', | ||
} | ||
|
||
// Utility function to generate placeholder src | ||
const getPlaceholderSrc = (placeholderColor: string): string => { | ||
const encodedColor = encodeURIComponent(placeholderColor) | ||
return `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%' viewBox='0 0 1 1' preserveAspectRatio='none'><rect width='1' height='1' fill='${encodedColor}' /></svg>` | ||
} | ||
|
||
const PLACEHOLDER_SRC = getPlaceholderSrc(LAZY_LOADING_CONFIG.placeholderColor) | ||
|
||
const initObserver = () => { | ||
if (observer) return | ||
|
||
observer = new IntersectionObserver( | ||
(entries) => { | ||
entries.forEach((entry) => { | ||
callbacks.forEach((cb) => cb(entry)) | ||
}) | ||
}, | ||
{ rootMargin: LAZY_LOADING_CONFIG.rootMargin }, | ||
) | ||
} | ||
|
||
const addCallback = (cb: (entry: IntersectionObserverEntry) => void) => { | ||
callbacks.push(cb) | ||
initObserver() | ||
} | ||
|
||
const removeCallback = (cb: (entry: IntersectionObserverEntry) => void) => { | ||
const index = callbacks.indexOf(cb) | ||
if (index > -1) { | ||
callbacks.splice(index, 1) | ||
} | ||
// If no callbacks remain, disconnect the observer | ||
if (callbacks.length === 0 && observer) { | ||
observer.disconnect() | ||
observer = null | ||
} | ||
} | ||
|
||
export function useLazyLoadImages() { | ||
const replaceImageUrls = useCallback((html: string): string => { | ||
return html.replace(/<img([^>]*)src="([^"]*)"([^>]*)>/g, (_, before, src, after) => { | ||
return `<img${before}data-src="${src}" src="${PLACEHOLDER_SRC}"${after}>` | ||
}) | ||
}, []) | ||
|
||
const LazyImage: FC<ImgHTMLAttributes<HTMLImageElement>> = ({ src, alt = '', width, height, style, ...props }) => { | ||
const imgRef = useRef<HTMLImageElement>(null) | ||
|
||
useEffect(() => { | ||
if (!imgRef.current || !src) return | ||
|
||
const handleIntersect = (entry: IntersectionObserverEntry) => { | ||
const img = entry.target as HTMLImageElement | ||
if (entry.isIntersecting && img.dataset.src) { | ||
img.src = img.dataset.src | ||
img.style.opacity = '1' | ||
observer?.unobserve(img) | ||
} | ||
} | ||
|
||
addCallback(handleIntersect) | ||
|
||
observer?.observe(imgRef.current) | ||
|
||
return () => { | ||
if (imgRef.current) { | ||
observer?.unobserve(imgRef.current) | ||
} | ||
|
||
removeCallback(handleIntersect) | ||
} | ||
}, [src]) | ||
|
||
return ( | ||
<img | ||
ref={imgRef} | ||
src={PLACEHOLDER_SRC} | ||
data-src={src} | ||
alt={alt} | ||
width={width} | ||
height={height} | ||
{...props} | ||
style={{ | ||
minHeight: LAZY_LOADING_CONFIG.minHeight, | ||
opacity: 0, | ||
transition: `opacity ${LAZY_LOADING_CONFIG.fadeInDuration}`, | ||
...style, | ||
}} | ||
/> | ||
) | ||
} | ||
|
||
const MemoizedLazyImage = memo(LazyImage) | ||
|
||
return { replaceImageUrls, LazyImage: MemoizedLazyImage } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.