Add memo optimization

This commit is contained in:
adilallo
2025-10-07 16:50:33 -06:00
parent e3861f6857
commit 2ed878af81
37 changed files with 1852 additions and 1480 deletions
+141 -130
View File
@@ -1,152 +1,163 @@
"use client";
import { useState, useEffect } from "react";
import React, { useState, useEffect, memo, useMemo, useCallback } from "react";
import ContentThumbnailTemplate from "./ContentThumbnailTemplate";
export default function RelatedArticles({
relatedPosts,
currentPostSlug,
slugOrder = [],
}) {
// Filter out the current post from related posts
const filteredPosts = relatedPosts.filter(
(post) => post.slug !== currentPostSlug,
);
const RelatedArticles = memo(
({ relatedPosts, currentPostSlug, slugOrder = [] }) => {
// Memoize filtered posts to prevent unnecessary re-computations
const filteredPosts = useMemo(
() => relatedPosts.filter((post) => post.slug !== currentPostSlug),
[relatedPosts, currentPostSlug]
);
const [currentIndex, setCurrentIndex] = useState(0);
const [progress, setProgress] = useState(0);
const [isMobile, setIsMobile] = useState(true);
const [currentIndex, setCurrentIndex] = useState(0);
const [progress, setProgress] = useState(0);
const [isMobile, setIsMobile] = useState(true);
// Check if we're on mobile (below lg breakpoint)
useEffect(() => {
const checkScreenSize = () => {
setIsMobile(window.innerWidth < 1024); // lg breakpoint is 1024px
};
// Memoize the mouse down handler to prevent unnecessary re-renders
const handleMouseDown = useCallback((e) => {
const slider = e.currentTarget;
const startX = e.pageX - slider.offsetLeft;
const scrollLeft = slider.scrollLeft;
checkScreenSize();
window.addEventListener("resize", checkScreenSize);
return () => window.removeEventListener("resize", checkScreenSize);
}, []);
const handleMouseMove = (e) => {
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 2;
slider.scrollLeft = scrollLeft - walk;
};
// Auto-advance every 3 seconds (only on mobile)
useEffect(() => {
if (filteredPosts.length <= 1 || !isMobile) return;
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
const interval = setInterval(() => {
setProgress(0);
setCurrentIndex((prev) => (prev + 1) % filteredPosts.length);
}, 3000);
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}, []);
return () => clearInterval(interval);
}, [filteredPosts.length, isMobile]);
// Memoize transform style to prevent unnecessary recalculations
const transformStyle = useMemo(
() => ({
transform: isMobile
? `translateX(calc(50% - 130px - ${currentIndex * 260}px))`
: "none",
scrollBehavior: !isMobile ? "smooth" : "auto",
}),
[isMobile, currentIndex]
);
// Progress animation (only on mobile)
useEffect(() => {
if (filteredPosts.length <= 1 || !isMobile) return;
// Memoize progress bar style calculation
const getProgressStyle = useCallback(
(index) => ({
width:
index === currentIndex
? `${progress}%`
: index < currentIndex
? "100%"
: "0%",
}),
[currentIndex, progress]
);
const progressInterval = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) {
return 0;
}
return prev + 1;
});
}, 30); // 30ms intervals for smooth animation
// Check if we're on mobile (below lg breakpoint)
useEffect(() => {
const checkScreenSize = () => {
setIsMobile(window.innerWidth < 1024); // lg breakpoint is 1024px
};
return () => clearInterval(progressInterval);
}, [currentIndex, filteredPosts.length, isMobile]);
checkScreenSize();
window.addEventListener("resize", checkScreenSize);
return () => window.removeEventListener("resize", checkScreenSize);
}, []);
if (filteredPosts.length === 0) {
return null;
}
// Auto-advance every 3 seconds (only on mobile)
useEffect(() => {
if (filteredPosts.length <= 1 || !isMobile) return;
return (
<section className="py-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)]">
<div className="flex flex-col gap-[var(--spacing-scale-032)] lg:gap-[51px]">
<h2 className="text-[32px] lg:text-[44px] leading-[110%] font-medium text-[var(--color-content-inverse-primary)] text-center">
Related Articles
</h2>
const interval = setInterval(() => {
setProgress(0);
setCurrentIndex((prev) => (prev + 1) % filteredPosts.length);
}, 3000);
{/* Horizontal Articles Row - Carousel on mobile, Scrollable slider on desktop */}
<div className="flex justify-center overflow-hidden">
<div
className={`flex gap-0 transition-transform duration-500 ease-in-out ${
!isMobile
? "overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing"
: ""
}`}
style={{
transform: isMobile
? `translateX(calc(50% - 130px - ${currentIndex * 260}px))`
: "none",
scrollBehavior: !isMobile ? "smooth" : "auto",
}}
onMouseDown={
!isMobile
? (e) => {
const slider = e.currentTarget;
const startX = e.pageX - slider.offsetLeft;
const scrollLeft = slider.scrollLeft;
return () => clearInterval(interval);
}, [filteredPosts.length, isMobile]);
const handleMouseMove = (e) => {
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 2;
slider.scrollLeft = scrollLeft - walk;
};
// Progress animation (only on mobile)
useEffect(() => {
if (filteredPosts.length <= 1 || !isMobile) return;
const handleMouseUp = () => {
document.removeEventListener(
"mousemove",
handleMouseMove,
);
document.removeEventListener("mouseup", handleMouseUp);
};
const progressInterval = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) {
return 0;
}
return prev + 1;
});
}, 30); // 30ms intervals for smooth animation
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
: undefined
}
>
{filteredPosts.map((relatedPost, index) => (
<div
key={relatedPost.slug}
className="flex flex-col items-center flex-shrink-0"
>
<ContentThumbnailTemplate
post={relatedPost}
variant="vertical"
slugOrder={slugOrder}
/>
</div>
))}
</div>
</div>
return () => clearInterval(progressInterval);
}, [currentIndex, filteredPosts.length, isMobile]);
{/* Progress bars - only show on mobile */}
{isMobile && (
<div className="flex justify-center gap-[var(--measures-spacing-008)] px-[var(--measures-spacing-064)]">
{filteredPosts.map((relatedPost, index) => (
<div
key={relatedPost.slug}
className="max-w-[var(--measures-spacing-056)] w-full h-[var(--measures-spacing-004)] bg-gray-200 rounded-full overflow-hidden"
>
if (filteredPosts.length === 0) {
return null;
}
return (
<section className="py-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)]">
<div className="flex flex-col gap-[var(--spacing-scale-032)] lg:gap-[51px]">
<h2 className="text-[32px] lg:text-[44px] leading-[110%] font-medium text-[var(--color-content-inverse-primary)] text-center">
Related Articles
</h2>
{/* Horizontal Articles Row - Carousel on mobile, Scrollable slider on desktop */}
<div className="flex justify-center overflow-hidden">
<div
className={`flex gap-0 transition-transform duration-500 ease-in-out ${
!isMobile
? "overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing"
: ""
}`}
style={transformStyle}
onMouseDown={!isMobile ? handleMouseDown : undefined}
>
{filteredPosts.map((relatedPost, index) => (
<div
className="h-full bg-gray-600 rounded-full transition-all duration-75 ease-linear"
style={{
width:
index === currentIndex
? `${progress}%`
: index < currentIndex
? "100%"
: "0%",
}}
/>
</div>
))}
key={relatedPost.slug}
className="flex flex-col items-center flex-shrink-0"
>
<ContentThumbnailTemplate
post={relatedPost}
variant="vertical"
slugOrder={slugOrder}
/>
</div>
))}
</div>
</div>
)}
</div>
</section>
);
}
{/* Progress bars - only show on mobile */}
{isMobile && (
<div className="flex justify-center gap-[var(--measures-spacing-008)] px-[var(--measures-spacing-064)]">
{filteredPosts.map((relatedPost, index) => (
<div
key={relatedPost.slug}
className="max-w-[var(--measures-spacing-056)] w-full h-[var(--measures-spacing-004)] bg-gray-200 rounded-full overflow-hidden"
>
<div
className="h-full bg-gray-600 rounded-full transition-all duration-75 ease-linear"
style={getProgressStyle(index)}
/>
</div>
))}
</div>
)}
</div>
</section>
);
}
);
RelatedArticles.displayName = "RelatedArticles";
export default RelatedArticles;