Files
GNX-WEB/frontEnd/components/pages/home/Story.tsx
Iliyan Angelov d7d7a2757a updates
2025-11-29 18:05:34 +02:00

346 lines
12 KiB
TypeScript

"use client";
import { useState, useEffect, useMemo, useRef } from "react";
import Image from "next/image";
import Link from "next/link";
import { getImageUrl } from "@/lib/imageUtils";
import { useCaseStudies } from "@/lib/hooks/useCaseStudy";
const Story = () => {
const [activeIndex, setActiveIndex] = useState(0);
const [activeImageIndex, setActiveImageIndex] = useState(0);
const [imagesLoaded, setImagesLoaded] = useState<Set<number>>(new Set());
const sectionRef = useRef<HTMLElement>(null);
const itemsRef = useRef<(HTMLDivElement | null)[]>([]);
const imageContainerRef = useRef<HTMLDivElement>(null);
// Fetch case studies from API with ordering and limit
const params = useMemo(() => ({
ordering: 'display_order',
page_size: 5
}), []);
const { caseStudies, loading, error } = useCaseStudies(params);
// Use only API data - no hardcoded fallback
const storyData = caseStudies;
// Preload all images to prevent blinking
useEffect(() => {
if (storyData.length === 0) return;
const preloadImages = () => {
// Mark first image as loaded immediately
setImagesLoaded((prev) => new Set(prev).add(0));
storyData.forEach((item, index) => {
if (index === 0) return; // Skip first image as it's already marked
const imageUrl = item.thumbnail ? getImageUrl(item.thumbnail) : '/images/case/one.png';
const img = new window.Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
// Small delay to ensure image is fully decoded
setTimeout(() => {
setImagesLoaded((prev) => {
const newSet = new Set(prev);
newSet.add(index);
return newSet;
});
}, 50);
};
img.onerror = () => {
// Still mark as loaded to prevent infinite waiting
setImagesLoaded((prev) => {
const newSet = new Set(prev);
newSet.add(index);
return newSet;
});
};
img.src = imageUrl;
});
};
preloadImages();
}, [storyData]);
// Update active image when it becomes loaded
useEffect(() => {
if (imagesLoaded.has(activeIndex) && activeImageIndex !== activeIndex) {
setActiveImageIndex(activeIndex);
}
}, [imagesLoaded, activeIndex, activeImageIndex]);
// Log when API data is loaded
useEffect(() => {
if (error) {
console.error('Error loading case studies:', error);
}
if (caseStudies.length > 0) {
console.log('Case studies loaded:', caseStudies.length);
}
}, [caseStudies, error]);
// Handle scroll-based active index update and image positioning
useEffect(() => {
if (!sectionRef.current || storyData.length === 0) return;
const updateActiveItem = () => {
const section = sectionRef.current;
const imageContainer = imageContainerRef.current;
if (!section || !imageContainer) return;
// Find which item is most visible
let mostVisibleIndex = activeIndex;
let maxVisibility = 0;
itemsRef.current.forEach((item, index) => {
if (!item) return;
const rect = item.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const itemCenter = rect.top + rect.height / 2;
const viewportCenter = viewportHeight / 2;
const distanceFromCenter = Math.abs(itemCenter - viewportCenter);
const visibility = 1 - (distanceFromCenter / viewportHeight);
if (visibility > maxVisibility && rect.top < viewportHeight && rect.bottom > 0) {
maxVisibility = visibility;
mostVisibleIndex = index;
}
});
if (mostVisibleIndex !== activeIndex) {
setActiveIndex(mostVisibleIndex);
// Only switch image if it's loaded
if (imagesLoaded.has(mostVisibleIndex) || mostVisibleIndex === 0) {
setActiveImageIndex(mostVisibleIndex);
}
}
// Position image container to align with active item
const activeItem = itemsRef.current[mostVisibleIndex];
if (activeItem && imageContainer) {
const sectionTop = section.offsetTop;
const itemTop = activeItem.offsetTop;
const itemHeight = activeItem.offsetHeight;
const containerHeight = imageContainer.offsetHeight || 400;
// Calculate the offset to center the image with the item
// Get the parent container to calculate relative position
const contentContainer = activeItem.closest('.tp-story__content');
if (contentContainer) {
const contentTop = (contentContainer as HTMLElement).offsetTop;
const relativeItemTop = itemTop - contentTop;
// Align image center with item center
const offset = relativeItemTop + (itemHeight / 2) - (containerHeight / 2);
// Apply transform to move the image container
imageContainer.style.transform = `translateY(${Math.max(0, offset)}px)`;
}
}
};
// Use Intersection Observer as backup
const observerOptions = {
root: null,
rootMargin: '-20% 0px -20% 0px',
threshold: [0, 0.25, 0.5, 0.75, 1]
};
const observer = new IntersectionObserver((entries) => {
let maxRatio = 0;
let mostVisibleIndex = activeIndex;
entries.forEach((entry) => {
const itemIndex = itemsRef.current.indexOf(entry.target as HTMLDivElement);
if (itemIndex !== -1 && entry.intersectionRatio > maxRatio) {
maxRatio = entry.intersectionRatio;
mostVisibleIndex = itemIndex;
}
});
if (mostVisibleIndex !== activeIndex && maxRatio > 0.1) {
setActiveIndex(mostVisibleIndex);
// Only switch image if it's loaded
if (imagesLoaded.has(mostVisibleIndex) || mostVisibleIndex === 0) {
setActiveImageIndex(mostVisibleIndex);
}
}
}, observerOptions);
// Observe all story items
itemsRef.current.forEach((item) => {
if (item) observer.observe(item);
});
// Update on scroll
window.addEventListener('scroll', updateActiveItem, { passive: true });
updateActiveItem(); // Initial call
return () => {
window.removeEventListener('scroll', updateActiveItem);
observer.disconnect();
};
}, [storyData.length, activeIndex]);
const handleMouseEnter = (index: number) => {
setActiveIndex(index);
// Only switch image if it's loaded, otherwise wait a bit
if (imagesLoaded.has(index) || index === 0) {
setActiveImageIndex(index);
} else {
// Wait for image to load before switching
const checkLoaded = setInterval(() => {
if (imagesLoaded.has(index)) {
setActiveImageIndex(index);
clearInterval(checkLoaded);
}
}, 50);
// Timeout after 1 second to prevent infinite waiting
setTimeout(() => {
clearInterval(checkLoaded);
setActiveImageIndex(index);
}, 1000);
}
};
// Show loading state
if (loading) {
return (
<section className="tp-story pt-120 pb-120 bg-black sticky-wrapper fade-wrapper">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center py-5">
<div className="spinner-border text-light" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-3 text-white">Loading case studies...</p>
</div>
</div>
</div>
</div>
</section>
);
}
// Show error or no data state
if (error || !storyData || storyData.length === 0) {
return (
<section className="tp-story pt-120 pb-120 bg-black sticky-wrapper fade-wrapper">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center py-5">
<h2 className="mt-8 title-anim text-white fw-7">
Enterprise Case Studies
</h2>
<p className="text-white mt-3">No data available</p>
</div>
</div>
</div>
</div>
</section>
);
}
return (
<section ref={sectionRef} className="tp-story pt-120 pb-120 bg-black sticky-wrapper fade-wrapper">
<div className="container">
<div className="row vertical-column-gap-md tp-story-row">
<div className="col-12 col-lg-5">
<div className="tp-story__content">
<h2 className="mt-8 title-anim text-white fw-7">
Enterprise Case Studies
</h2>
<div className="tp-story__items">
{storyData.map((item, index) => {
return (
<div
key={item.id || index}
ref={(el) => {
itemsRef.current[index] = el;
}}
className={`tp-story__single fade-top ${
index === activeIndex ? "active" : ""
}`}
onMouseEnter={() => handleMouseEnter(index)}
>
<p className="fw-6 mt-8">
<Link href={`/case-study/${item.slug}`}>
{item.category_name || item.category?.name || "Case Study"}
</Link>
</p>
<h5 className="fw-4 mt-12 mb-12 text-white">
<Link href={`/case-study/${item.slug}`} className="text-white">
{item.title}
</Link>
</h5>
<p className="text-xs">{item.excerpt || item.description?.substring(0, 150) + '...'}</p>
</div>
);
})}
</div>
</div>
</div>
<div className="col-12 col-lg-7 col-xxl-6 offset-xxl-1 d-none d-lg-block">
<div className="tp-story__thumbs">
<div
ref={imageContainerRef}
className="tp-story-thumb-wrapper"
>
{storyData.map((item, index) => {
// Get the image URL using the utility function
const imageUrl = item.thumbnail ? getImageUrl(item.thumbnail) : '/images/case/one.png';
const isActive = index === activeImageIndex;
const isLoaded = imagesLoaded.has(index);
return (
<div
key={`image-${item.id || index}`}
className={`tp-story-thumb ${isActive ? "thumb-active" : ""}`}
data-loaded={isLoaded}
>
<Link href={`/case-study/${item.slug}`} className="w-100">
<Image
src={imageUrl}
width={600}
height={300}
className="w-100 mh-300"
alt={item.title || "Case Study"}
priority={index === 0}
loading={index === 0 ? 'eager' : 'lazy'}
style={{
display: 'block',
width: '100%',
height: 'auto',
objectFit: 'cover',
opacity: isLoaded ? 1 : 0,
transition: 'opacity 0.3s ease'
}}
onLoad={() => {
if (!isLoaded) {
setImagesLoaded((prev) => {
const newSet = new Set(prev);
newSet.add(index);
return newSet;
});
}
}}
/>
</Link>
</div>
);
})}
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default Story;