Related Article section implemented

This commit is contained in:
adilallo
2025-09-11 14:06:31 -06:00
parent ec2db8be22
commit 8a31671bbc
5 changed files with 152 additions and 40 deletions
+8 -23
View File
@@ -1,8 +1,8 @@
import { notFound } from "next/navigation";
import Link from "next/link";
import { getBlogPostBySlug, getAllPosts } from "../../../lib/contentProcessor";
import ContentThumbnailTemplate from "../../components/ContentThumbnailTemplate";
import ContentBanner from "../../components/ContentBanner";
import RelatedArticles from "../../components/RelatedArticles";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
/**
@@ -70,9 +70,9 @@ export default async function BlogPostPage({ params }) {
notFound();
}
// Get related posts (for now, just get other posts)
// Get related articles (for now, just get other posts)
const allPosts = getAllPosts();
const relatedPosts = allPosts.filter((p) => p.slug !== post.slug).slice(0, 3); // Show up to 3 related posts
const relatedArticles = allPosts; // Pass all posts to RelatedArticles component for filtering
return (
<div className="min-h-screen bg-[#F4F3F1] relative overflow-hidden">
@@ -122,26 +122,11 @@ export default async function BlogPostPage({ params }) {
</div>
</article>
{/* Related Posts Section */}
{relatedPosts.length > 0 && (
<section className="bg-white border-t border-gray-200">
<div className="max-w-6xl mx-auto px-4 py-12">
<h2 className="text-3xl font-bold text-gray-900 mb-8 text-center">
Related Articles
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{relatedPosts.map((relatedPost) => (
<ContentThumbnailTemplate
key={relatedPost.slug}
post={relatedPost}
variant="vertical"
/>
))}
</div>
</div>
</section>
)}
{/* Related Articles Section */}
<RelatedArticles
relatedPosts={relatedArticles}
currentPostSlug={post.slug}
/>
</div>
);
}
+10 -7
View File
@@ -14,13 +14,16 @@ const ContentContainer = ({ post, width = "200px", size = "responsive" }) => {
if (!slug) return icons[0];
// Use the same hash logic as background images to ensure matching
const hash = slug.split("").reduce((a, b) => {
a = (a << 5) - a + b.charCodeAt(0);
return a & a;
}, 0);
return icons[Math.abs(hash) % icons.length];
// Use the same cycling logic as background images to ensure matching
const slugOrder = [
"building-community-trust",
"operational-security-mutual-aid",
"making-decisions-without-hierarchy",
"resolving-active-conflicts",
];
const index = slugOrder.indexOf(slug);
const finalIndex = index >= 0 ? index % icons.length : 0;
return icons[finalIndex];
};
const iconImage = getIconImage(post.slug);
+10 -10
View File
@@ -32,16 +32,16 @@ const ContentThumbnailTemplate = ({
if (!slug) return images[0];
// Use the slug to deterministically select an image
// More robust hash function using djb2 algorithm
let hash = 5381;
for (let i = 0; i < slug.length; i++) {
hash = (hash << 5) + hash + slug.charCodeAt(i);
}
// Ensure positive number and get index
const index = Math.abs(hash) % images.length;
return images[index];
// Simple cycling approach to ensure different styles
const slugOrder = [
"building-community-trust",
"operational-security-mutual-aid",
"making-decisions-without-hierarchy",
"resolving-active-conflicts",
];
const index = slugOrder.indexOf(slug);
const finalIndex = index >= 0 ? index % images.length : 0;
return images[finalIndex];
};
const backgroundImage = getBackgroundImage(post.slug, variant);
+102
View File
@@ -0,0 +1,102 @@
"use client";
import { useState, useEffect } from "react";
import ContentThumbnailTemplate from "./ContentThumbnailTemplate";
export default function RelatedArticles({ relatedPosts, currentPostSlug }) {
// Filter out the current post from related posts
const filteredPosts = relatedPosts.filter(
(post) => post.slug !== currentPostSlug
);
const [currentIndex, setCurrentIndex] = useState(0);
const [progress, setProgress] = useState(0);
// Auto-advance every 3 seconds
useEffect(() => {
if (filteredPosts.length <= 1) return;
const interval = setInterval(() => {
setProgress(0);
setCurrentIndex((prev) => (prev + 1) % filteredPosts.length);
}, 3000);
return () => clearInterval(interval);
}, [filteredPosts.length]);
// Progress animation
useEffect(() => {
if (filteredPosts.length <= 1) return;
const progressInterval = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) {
return 0;
}
return prev + 1;
});
}, 30); // 30ms intervals for smooth animation
return () => clearInterval(progressInterval);
}, [currentIndex, filteredPosts.length]);
if (filteredPosts.length === 0) {
return null;
}
return (
<section className="py-[var(--spacing-scale-032)]">
<div className="flex flex-col gap-[var(--spacing-scale-032)]">
<h2 className="text-[32px] leading-[110%] font-medium text-[var(--color-content-inverse-primary)] text-center">
Related Articles
</h2>
{/* Horizontal Articles Row - All Visible with Centering */}
<div className="flex justify-center overflow-hidden">
<div
className="flex gap-0 transition-transform duration-500 ease-in-out"
style={{
transform: `translateX(calc(50% - 130px - ${
currentIndex * 260
}px))`,
}}
>
{filteredPosts.map((relatedPost, index) => (
<div
key={relatedPost.slug}
className="flex flex-col items-center"
>
<ContentThumbnailTemplate
post={relatedPost}
variant="vertical"
/>
</div>
))}
</div>
</div>
{/* Three separate progress bars below the carousel */}
<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={{
width:
index === currentIndex
? `${progress}%`
: index < currentIndex
? "100%"
: "0%",
}}
/>
</div>
))}
</div>
</div>
</section>
);
}