update
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
import { useCaseStudies } from "@/lib/hooks/useCaseStudy";
|
||||
import { API_CONFIG } from "@/lib/config/api";
|
||||
|
||||
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(() => ({
|
||||
@@ -18,86 +21,236 @@ const Story = () => {
|
||||
|
||||
const { caseStudies, loading, error } = useCaseStudies(params);
|
||||
|
||||
// Fallback to static data if API fails or is loading
|
||||
const staticStoryData = [
|
||||
{
|
||||
id: 1,
|
||||
category_name: "Financial Services",
|
||||
title: "Banking System Modernization",
|
||||
excerpt: "Complete digital transformation of legacy banking systems with enhanced security and real-time processing capabilities.",
|
||||
thumbnail: "/images/case/one.png",
|
||||
slug: "banking-system-modernization",
|
||||
display_order: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category_name: "Healthcare",
|
||||
title: "Patient Management System",
|
||||
excerpt: "Enterprise-grade patient management system with HIPAA compliance and seamless integration across multiple healthcare facilities.",
|
||||
thumbnail: "/images/case/two.png",
|
||||
slug: "patient-management-system",
|
||||
display_order: 2,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category_name: "Manufacturing",
|
||||
title: "Supply Chain Optimization",
|
||||
excerpt: "Advanced supply chain management system with real-time tracking, predictive analytics, and automated inventory management.",
|
||||
thumbnail: "/images/case/three.png",
|
||||
slug: "supply-chain-optimization",
|
||||
display_order: 3,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category_name: "E-commerce",
|
||||
title: "Multi-Platform Integration",
|
||||
excerpt: "Seamless integration of multiple e-commerce platforms with unified inventory management and real-time synchronization.",
|
||||
thumbnail: "/images/case/four.png",
|
||||
slug: "multi-platform-integration",
|
||||
display_order: 4,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category_name: "Education",
|
||||
title: "Learning Management System",
|
||||
excerpt: "Comprehensive LMS with advanced analytics, automated grading, and seamless integration with existing educational tools.",
|
||||
thumbnail: "/images/case/five.png",
|
||||
slug: "learning-management-system",
|
||||
display_order: 5,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
// Use only API data - no hardcoded fallback
|
||||
const storyData = caseStudies;
|
||||
|
||||
// Preload all images to prevent blinking
|
||||
useEffect(() => {
|
||||
if (storyData.length === 0) return;
|
||||
|
||||
// Use API data if available, otherwise use static data
|
||||
const storyData = caseStudies.length > 0 ? caseStudies : staticStoryData;
|
||||
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 (caseStudies.length > 0) {
|
||||
if (error) {
|
||||
console.error('Error loading case studies:', error);
|
||||
}
|
||||
}, [caseStudies]);
|
||||
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);
|
||||
setActiveImageIndex(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 className="tp-story pt-120 pb-120 bg-black sticky-wrapper fade-wrapper">
|
||||
<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">
|
||||
<div className="row vertical-column-gap-md tp-story-row">
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="tp-story__content sticky-item">
|
||||
<div className="tp-story__content">
|
||||
<h2 className="mt-8 title-anim text-white fw-7">
|
||||
Enterprise Case Studies
|
||||
</h2>
|
||||
@@ -105,7 +258,10 @@ const Story = () => {
|
||||
{storyData.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
key={item.id || index}
|
||||
ref={(el) => {
|
||||
itemsRef.current[index] = el;
|
||||
}}
|
||||
className={`tp-story__single fade-top ${
|
||||
index === activeIndex ? "active" : ""
|
||||
}`}
|
||||
@@ -113,13 +269,13 @@ const Story = () => {
|
||||
>
|
||||
<p className="fw-6 mt-8">
|
||||
<Link href={`/case-study/${item.slug}`}>
|
||||
{item.category_name || "Case Study"}
|
||||
{item.category_name || item.category?.name || "Case Study"}
|
||||
</Link>
|
||||
</p>
|
||||
<h5 className="fw-4 mt-12 mb-12 text-white">
|
||||
{item.title}
|
||||
</h5>
|
||||
<p className="text-xs">{item.excerpt}</p>
|
||||
<p className="text-xs">{item.excerpt || item.description?.substring(0, 150) + '...'}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -127,43 +283,53 @@ const Story = () => {
|
||||
</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 sticky-item">
|
||||
{storyData.map((item, index) => {
|
||||
// Get the image URL - handle different scenarios
|
||||
let imageUrl;
|
||||
if (item.thumbnail) {
|
||||
if (item.thumbnail.startsWith('http')) {
|
||||
// Full URL (external)
|
||||
imageUrl = item.thumbnail;
|
||||
} else if (item.thumbnail.startsWith('/media')) {
|
||||
// Relative path starting with /media
|
||||
imageUrl = `${API_CONFIG.BASE_URL}${item.thumbnail}`;
|
||||
} else {
|
||||
// Just filename or relative path
|
||||
imageUrl = `${API_CONFIG.MEDIA_URL}/${item.thumbnail}`;
|
||||
}
|
||||
} else {
|
||||
// Fallback to static image
|
||||
imageUrl = getValidImageUrl('/images/case/one.png', FALLBACK_IMAGES.CASE_STUDY);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`tp-story-thumb ${
|
||||
index === activeImageIndex ? "thumb-active" : ""
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
width={600}
|
||||
height={300}
|
||||
className="w-100 mh-300"
|
||||
alt={item.title || "Case Study"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<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}
|
||||
>
|
||||
<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;
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user