Shared useinview

This commit is contained in:
Amritanshu Agrawal 2024-09-27 11:01:36 +05:30
parent 127e024374
commit d64da34b1e
7 changed files with 221 additions and 135 deletions

View File

@ -1,46 +1,13 @@
'use client'; 'use client';
import { useRef } from 'react';
import Image from 'next/image';
import pralinePic from '/public/images/about-us/01-praline.jpg'; import pralinePic from '/public/images/about-us/01-praline.jpg';
import prAmPic from '/public/images/about-us/02-priyanka-amritanshu.jpg'; import prAmPic from '/public/images/about-us/02-priyanka-amritanshu.jpg';
import dryingPic from '/public/images/about-us/03-drying.jpg'; import dryingPic from '/public/images/about-us/03-drying.jpg';
import chefPic from '/public/images/about-us/04-chef.jpg'; import chefPic from '/public/images/about-us/04-chef.jpg';
import barkPic from '/public/images/about-us/05-bark.jpg'; import barkPic from '/public/images/about-us/05-bark.jpg';
import italyPic from '/public/images/about-us/06-italy.jpg'; import italyPic from '/public/images/about-us/06-italy.jpg';
import useInView from '@/hooks/useInView'; import { AnimatedImage } from '@/components/animated-image';
export default function AboutUsPage() { export default function AboutUsPage() {
const pralinePicRef = useRef<HTMLImageElement>(null);
const isPralinePic = useInView(pralinePicRef, {
threshold: 0.1,
triggerOnce: true,
});
const prAmPicRef = useRef<HTMLImageElement>(null);
const isPrAmPic = useInView(prAmPicRef, {
threshold: 0.1,
triggerOnce: true,
});
const dryingPicRef = useRef<HTMLImageElement>(null);
const isDryingPic = useInView(dryingPicRef, {
threshold: 0.1,
triggerOnce: true,
});
const chefPicRef = useRef<HTMLImageElement>(null);
const isChefPic = useInView(chefPicRef, {
threshold: 0.1,
triggerOnce: true,
});
const barkPicRef = useRef<HTMLImageElement>(null);
const isBarkPic = useInView(barkPicRef, {
threshold: 0.1,
triggerOnce: true,
});
const italyPicRef = useRef<HTMLImageElement>(null);
const isItalyPic = useInView(italyPicRef, {
threshold: 0.1,
triggerOnce: true,
});
return ( return (
<div className='overflow-x-hidden bg-white pt-28'> <div className='overflow-x-hidden bg-white pt-28'>
<section className='flex flex-col items-center'> <section className='flex flex-col items-center'>
@ -58,27 +25,27 @@ export default function AboutUsPage() {
chocolate-making. chocolate-making.
</div> </div>
<div className='w-full'> <div className='w-full'>
<Image <AnimatedImage
ref={pralinePicRef}
src={pralinePic} src={pralinePic}
alt='Praline' alt='Praline'
className={`w-full h-auto zoom ${isPralinePic ? 'post' : 'pre'}`} className='w-full h-auto'
sizes='100vw' loading='lazy'
style={{ // {`w-full h-auto zoom ${isPralinePic ? 'post' : 'pre'}`}
width: '100%', // sizes='100vw'
height: 'auto', // style={{
}} // width: '100%',
// height: 'auto',
// }}
/> />
</div> </div>
</section> </section>
<section className='flex flex-col md:flex-row items-center'> <section className='flex flex-col md:flex-row items-center'>
{/* Left Column - Image */} {/* Left Column - Image */}
<div className='w-full md:w-1/2'> <div className='w-full md:w-1/2'>
<Image <AnimatedImage
ref={prAmPicRef}
src={prAmPic} src={prAmPic}
alt='Left Side Image' alt='Left Side Image'
className={`w-full h-auto zoom ${isPrAmPic ? 'post' : 'pre'}`} className='w-full h-auto'
sizes='100vw' sizes='100vw'
style={{ style={{
width: '100%', width: '100%',
@ -127,11 +94,10 @@ export default function AboutUsPage() {
</div> </div>
</div> </div>
<div className='w-full md:w-1/2'> <div className='w-full md:w-1/2'>
<Image <AnimatedImage
ref={dryingPicRef}
src={dryingPic} src={dryingPic}
alt='Beans Drying' alt='Beans Drying'
className={`w-full h-auto zoom ${isDryingPic ? 'post' : 'pre'}`} className='w-full h-auto'
sizes='100vw' sizes='100vw'
style={{ style={{
width: '100%', width: '100%',
@ -143,11 +109,10 @@ export default function AboutUsPage() {
{/* Fourth Three-Column Layout (Collections) */} {/* Fourth Three-Column Layout (Collections) */}
<section className='flex flex-col md:flex-row items-center'> <section className='flex flex-col md:flex-row items-center'>
<div className='w-full md:w-1/2'> <div className='w-full md:w-1/2'>
<Image <AnimatedImage
ref={chefPicRef}
src={chefPic} src={chefPic}
alt='Beans Drying' alt='Beans Drying'
className={`w-full h-auto zoom ${isChefPic ? 'post' : 'pre'}`} className='w-full h-auto'
sizes='100vw' sizes='100vw'
style={{ style={{
width: '100%', width: '100%',
@ -156,11 +121,11 @@ export default function AboutUsPage() {
/> />
</div> </div>
<div className='w-full md:w-1/2'> <div className='w-full md:w-1/2'>
<Image <AnimatedImage
ref={barkPicRef}
src={barkPic} src={barkPic}
alt='Beans Drying' alt='Beans Drying'
className={`w-full h-auto zoom ${isBarkPic ? 'post' : 'pre'}`} className='w-full h-auto'
loading='lazy'
sizes='100vw' sizes='100vw'
style={{ style={{
width: '100%', width: '100%',
@ -188,11 +153,11 @@ export default function AboutUsPage() {
that define the world of chocolate making. that define the world of chocolate making.
</div> </div>
<div className='flex w-full'> <div className='flex w-full'>
<Image <AnimatedImage
ref={italyPicRef}
src={italyPic} src={italyPic}
alt='Training' alt='Training'
className={`w-full h-auto zoom ${isItalyPic ? 'post' : 'pre'}`} className='w-full h-auto'
loading='lazy'
sizes='100vw' sizes='100vw'
style={{ style={{
width: '100%', width: '100%',

View File

@ -1,6 +1,3 @@
'use client';
import { useRef } from 'react';
import Image from 'next/image';
import brandStoryPic from '/public/images/homepage/brand-story.jpg'; import brandStoryPic from '/public/images/homepage/brand-story.jpg';
import { HomepageVideo } from '@/components/homepage-video'; import { HomepageVideo } from '@/components/homepage-video';
import { HeroSwiper } from '@/components/swiper'; import { HeroSwiper } from '@/components/swiper';
@ -9,15 +6,9 @@ import { ChocolateCategories } from '@/components/category-slider';
import { InstagramFeed } from '@/components/instagram'; import { InstagramFeed } from '@/components/instagram';
import { Spotlight } from '@/components/spotlight'; import { Spotlight } from '@/components/spotlight';
import { AnimatedText } from '@/components/animated-text'; import { AnimatedText } from '@/components/animated-text';
import useInView from '@/hooks/useInView'; import { AnimatedImage } from '@/components/animated-image';
export default function HomePage() { export default function HomePage() {
const brandStoryPicRef = useRef<HTMLImageElement>(null);
const isBrandStoryPic = useInView(brandStoryPicRef, {
threshold: 0.1,
triggerOnce: true,
});
return ( return (
<div className='overflow-x-hidden'> <div className='overflow-x-hidden'>
<HeroSwiper /> <HeroSwiper />
@ -53,16 +44,17 @@ export default function HomePage() {
</div> </div>
{/* Right Column - Image */} {/* Right Column - Image */}
<div className='w-full md:w-1/2'> <div className='w-full md:w-1/2'>
<Image <AnimatedImage
ref={brandStoryPicRef} // ref={brandStoryPicRef}
src={brandStoryPic} src={brandStoryPic}
alt='Right Side Image' alt='Right Side Image'
className={`w-full h-auto zoom ${isBrandStoryPic ? 'post' : 'pre'}`} className='w-full h-auto'
sizes='100vw' // {`w-full h-auto zoom ${isBrandStoryPic ? 'post' : 'pre'}`}
style={{ // sizes='100vw'
width: '100%', // style={{
height: 'auto', // width: '100%',
}} // height: 'auto',
// }}
/> />
</div> </div>
</section> </section>

View File

@ -1,38 +1,38 @@
// src/components/AnimatedImage.tsx // src/components/AnimatedImage.tsx
import React, { ImgHTMLAttributes, useRef } from 'react'; 'use client';
import React from 'react';
import useInView from '@/hooks/useInView'; import useInView from '@/hooks/useInView';
import Image, { StaticImageData } from 'next/image';
interface AnimatedImageProps extends ImgHTMLAttributes<HTMLImageElement> { interface AnimatedImageProps {
src: string; src: string | StaticImageData;
alt: string; alt: string;
className?: string; className?: string;
threshold?: number; // New prop threshold?: number;
// Additional props if needed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
} }
const AnimatedImage: React.FC<AnimatedImageProps> = ({ export function AnimatedImage({
src, src,
alt, alt,
className = '', className = '',
threshold, threshold,
...props ...props
}) => { }: AnimatedImageProps) {
const ref = useRef<HTMLImageElement>(null); const [ref, isVisible] = useInView({
const isVisible = useInView(ref, {
threshold: threshold ?? 0.1, threshold: threshold ?? 0.1,
triggerOnce: true, triggerOnce: true,
}); });
return ( return (
<img <Image
ref={ref} ref={ref}
src={src} src={src}
alt={alt} alt={alt}
className={`opacity-0 transform scale-95 transition-opacity transition-transform duration-700 ease-out ${ className={`zoom ${isVisible ? 'post' : 'pre'} ${className}`}
isVisible ? 'opacity-100 scale-100' : ''
} ${className}`}
{...props} {...props}
/> />
); );
}; }
export default AnimatedImage;

View File

@ -1,5 +1,5 @@
// src/components/AnimatedText.tsx 'use client';
import { ReactNode, useRef } from 'react'; import { ReactNode } from 'react';
import useInView from '@/hooks/useInView'; import useInView from '@/hooks/useInView';
interface AnimatedTextProps { interface AnimatedTextProps {
@ -7,7 +7,7 @@ interface AnimatedTextProps {
className?: string; className?: string;
startClass?: string; startClass?: string;
finishClass?: string; finishClass?: string;
threshold?: number; // New prop threshold?: number;
} }
export function AnimatedText({ export function AnimatedText({
@ -17,8 +17,7 @@ export function AnimatedText({
finishClass, finishClass,
threshold, threshold,
}: AnimatedTextProps) { }: AnimatedTextProps) {
const ref = useRef<HTMLDivElement>(null); const [ref, isVisible] = useInView({
const isVisible = useInView(ref, {
threshold: threshold ?? 0.1, threshold: threshold ?? 0.1,
triggerOnce: true, triggerOnce: true,
}); });

View File

@ -0,0 +1,80 @@
import { useEffect } from 'react';
// eslint-disable-next-line no-unused-vars
type Callback = (entry: IntersectionObserverEntry) => void;
class IntersectionObserverManager {
private observer: IntersectionObserver;
private callbacks: Map<Element, Callback>;
// eslint-disable-next-line no-undef
constructor(options: IntersectionObserverInit) {
this.callbacks = new Map();
this.observer = new IntersectionObserver(
this.handleIntersect.bind(this),
options,
);
}
private handleIntersect(entries: IntersectionObserverEntry[]) {
entries.forEach((entry) => {
const callback = this.callbacks.get(entry.target);
if (callback) {
callback(entry);
}
});
}
observe(element: Element, callback: Callback) {
if (element && callback) {
this.callbacks.set(element, callback);
this.observer.observe(element);
}
}
unobserve(element: Element) {
if (element) {
this.callbacks.delete(element);
this.observer.unobserve(element);
}
}
disconnect() {
this.observer.disconnect();
this.callbacks.clear();
}
}
let manager: IntersectionObserverManager | null = null;
// eslint-disable-next-line no-undef
const getObserverManager = (options: IntersectionObserverInit) => {
if (!manager) {
manager = new IntersectionObserverManager(options);
}
return manager;
};
export const useSharedIntersectionObserver = (
// eslint-disable-next-line no-undef
options: IntersectionObserverInit,
) => {
useEffect(() => {
return () => {
// Cleanup logic if needed
// For example, disconnect the observer when the component unmounts entirely
// This depends on your application's lifecycle
};
}, []);
if (typeof window === 'undefined') {
// Return a dummy manager for SSR
return {
observe: () => {},
unobserve: () => {},
disconnect: () => {},
} as unknown as IntersectionObserverManager;
}
return getObserverManager(options);
};

View File

@ -1,63 +1,50 @@
import { useState, useEffect, RefObject } from 'react'; 'use client';
import { useState, useEffect, useRef, RefObject } from 'react';
import { useSharedIntersectionObserver } from './shared-intersection-observer';
interface UseInViewOptions { // eslint-disable-next-line no-undef
threshold?: number | number[]; interface UseInViewOptions extends IntersectionObserverInit {
triggerOnce?: boolean; triggerOnce?: boolean;
} }
const useInView = ( const useInView = (
ref: RefObject<Element>, options: UseInViewOptions,
options: UseInViewOptions = { threshold: 0.1, triggerOnce: true }, ): [RefObject<HTMLImageElement>, boolean] => {
): boolean => { const [isInView, setIsInView] = useState<boolean>(false);
const { threshold = 0.1, triggerOnce = true } = options; const elementRef = useRef<HTMLImageElement>(null);
const [isVisible, setIsVisible] = useState<boolean>(false); const observerManager = useSharedIntersectionObserver(options);
// Validate threshold
const isValidThreshold =
typeof threshold === 'number'
? threshold >= 0 && threshold <= 1
: Array.isArray(threshold) && threshold.every((t) => t >= 0 && t <= 1);
if (!isValidThreshold) {
console.warn(
'Invalid threshold value passed to useInView. It should be between 0 and 1.',
);
}
useEffect(() => { useEffect(() => {
const element = ref.current; const element = elementRef.current;
if (!element) return; if (!element) return;
const prefersReducedMotion = window.matchMedia( const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)', '(prefers-reduced-motion: reduce)',
); );
if (prefersReducedMotion.matches) { if (prefersReducedMotion.matches) {
setIsVisible(true); setIsInView(true);
if (options.triggerOnce) {
observerManager.unobserve(element);
}
return; return;
} }
const observer = new IntersectionObserver( const callback = (entry: IntersectionObserverEntry) => {
(entries) => { if (entry.isIntersecting) {
entries.forEach((entry) => { setIsInView(true);
if (entry.isIntersecting) { if (options.triggerOnce) {
setIsVisible(true); observerManager.unobserve(element);
if (triggerOnce) { }
observer.unobserve(entry.target); }
} };
}
});
},
{ threshold },
);
observer.observe(element); observerManager.observe(element, callback);
return () => { return () => {
if (element) observer.unobserve(element); observerManager.unobserve(element);
}; };
}, [ref, threshold, triggerOnce]); }, [observerManager]);
return isVisible; return [elementRef, isInView];
}; };
export default useInView; export default useInView;

View File

@ -0,0 +1,63 @@
import { useState, useEffect, RefObject } from 'react';
interface UseInViewOptions {
threshold?: number | number[];
triggerOnce?: boolean;
}
const useInView = (
ref: RefObject<Element>,
options: UseInViewOptions = { threshold: 0.1, triggerOnce: true },
): boolean => {
const { threshold = 0.1, triggerOnce = true } = options;
const [isVisible, setIsVisible] = useState<boolean>(false);
// Validate threshold
const isValidThreshold =
typeof threshold === 'number'
? threshold >= 0 && threshold <= 1
: Array.isArray(threshold) && threshold.every((t) => t >= 0 && t <= 1);
if (!isValidThreshold) {
console.warn(
'Invalid threshold value passed to useInView. It should be between 0 and 1.',
);
}
useEffect(() => {
const element = ref.current;
if (!element) return;
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)',
);
if (prefersReducedMotion.matches) {
setIsVisible(true);
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true);
if (triggerOnce) {
observer.unobserve(entry.target);
}
}
});
},
{ threshold },
);
observer.observe(element);
return () => {
if (element) observer.unobserve(element);
};
}, [ref, threshold, triggerOnce]);
return isVisible;
};
export default useInView;