Skip to content

Commit

Permalink
feat(cow-fi): lazy load cms images /learn/ pages (#4906)
Browse files Browse the repository at this point in the history
* 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
fairlighteth authored Sep 25, 2024
1 parent 47b7115 commit 897ce91
Show file tree
Hide file tree
Showing 39 changed files with 865 additions and 520 deletions.
31 changes: 31 additions & 0 deletions apps/cow-fi/components/ArticlesList.tsx
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>
)
109 changes: 109 additions & 0 deletions apps/cow-fi/components/CategoryLinks.tsx
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>
)
66 changes: 66 additions & 0 deletions apps/cow-fi/components/LazySVG.tsx
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
6 changes: 3 additions & 3 deletions apps/cow-fi/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const Input = styled.input`
min-height: 56px;
border: 2px solid transparent;
font-size: 21px;
color: ${Color.neutral50};
color: ${Color.neutral40};
width: 100%;
background: ${Color.neutral90};
border-radius: 56px;
Expand Down Expand Up @@ -215,7 +215,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({ articles }) => {
useEffect(() => {
if (query.trim()) {
const filtered = (articles || []).filter((article) =>
article.attributes?.title?.toLowerCase().includes(query.toLowerCase())
article.attributes?.title?.toLowerCase().includes(query.toLowerCase()),
)
setFilteredArticles(filtered)
} else {
Expand All @@ -230,7 +230,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({ articles }) => {
<HighlightedText key={index}>{part}</HighlightedText>
) : (
<span key={index}>{part}</span>
)
),
)
}

Expand Down
23 changes: 0 additions & 23 deletions apps/cow-fi/const/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,27 +55,4 @@ interface MetaTokenDetails extends TokenInfoExcluded {
change24hTrimmed: string
}

export const META_DESCRIPTION_TEMPLATES = [
({ name, symbol, priceUsd, change24hTrimmed, volume, marketCap }: MetaTokenDetails) =>
`Moo-ve over to ${name} (${symbol})! Grazing at $${priceUsd} with ${change24hTrimmed}% change in 24h. Trading volume: $${volume}. Their market cap: $${marketCap}. Learn about ${symbol}'s pasture.`,

({ name, symbol, priceUsd, change24hTrimmed, volume, marketCap }: MetaTokenDetails) =>
`The grass is greener with ${name} (${symbol})! At $${priceUsd}, with ${change24hTrimmed}% change in 24h. Trading volume: $${volume}. Their market cap: $${marketCap}. Discover more about ${symbol}.`,

({ name, symbol, priceUsd, change24hTrimmed, volume, marketCap }: MetaTokenDetails) =>
`Interested in ${name} (${symbol})? Priced at $${priceUsd}, with ${change24hTrimmed}% change in 24h. Trading volume: $${volume}. Their market cap: $${marketCap}. Learn more about ${symbol}!`,

({ name, symbol, priceUsd, change24hTrimmed, volume, marketCap }: MetaTokenDetails) =>
`Graze on this: ${name} (${symbol}) at $${priceUsd}, with ${change24hTrimmed}% change in 24h. Trading volume: $${volume}. They boast a market cap of $${marketCap}. Learn about ${symbol}.`,

({ name, symbol, priceUsd, change24hTrimmed, volume, marketCap }: MetaTokenDetails) =>
`Ever heard of ${name} (${symbol})? At $${priceUsd}, they've marked a ${change24hTrimmed}% change in 24h. Trading volume: $${volume}. Their market cap: $${marketCap}. Get to know them.`,

({ name, symbol, priceUsd, change24hTrimmed, volume, marketCap }: MetaTokenDetails) =>
`Check out ${name} (${symbol})! Grazing at $${priceUsd}, with ${change24hTrimmed}% change in 24h. Trading volume: $${volume}. Their market cap: $${marketCap}. Discover ${symbol}'s secrets.`,

({ name, symbol, priceUsd, change24hTrimmed, volume, marketCap }: MetaTokenDetails) =>
`Latest on ${name} (${symbol}): priced at $${priceUsd}. Experienced a ${change24hTrimmed}% change in 24h. Trading volume: $${volume}. Their market cap: $${marketCap}. Learn more.`,
]

export type SiteConfig = typeof CONFIG
108 changes: 108 additions & 0 deletions apps/cow-fi/hooks/useLazyLoadImages.tsx
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 }
}
3 changes: 2 additions & 1 deletion apps/cow-fi/modules/analytics/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { initCowAnalyticsGoogle } from '@cowprotocol/analytics'

export const cowAnalytics = initCowAnalyticsGoogle()
// Loads Analytics with GTM
export const cowAnalytics = initCowAnalyticsGoogle(true)

export enum Category {
HOME = 'Homepage',
Expand Down
Loading

0 comments on commit 897ce91

Please sign in to comment.