feat: add mobile optimization with hooks and touch components
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef, type RefObject } from 'react';
|
||||
|
||||
interface UseIntersectionObserverOptions {
|
||||
threshold?: number | number[];
|
||||
root?: Element | null;
|
||||
rootMargin?: string;
|
||||
freezeOnceVisible?: boolean;
|
||||
}
|
||||
|
||||
export function useIntersectionObserver<T extends Element>(
|
||||
options: UseIntersectionObserverOptions = {}
|
||||
): [RefObject<T | null>, boolean] {
|
||||
const { threshold = 0, root = null, rootMargin = '0px', freezeOnceVisible = false } = options;
|
||||
|
||||
const elementRef = useRef<T | null>(null);
|
||||
const [isIntersecting, setIsIntersecting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
|
||||
if (freezeOnceVisible && isIntersecting) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setIsIntersecting(entry.isIntersecting);
|
||||
},
|
||||
{ threshold, root, rootMargin }
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [threshold, root, rootMargin, freezeOnceVisible, isIntersecting]);
|
||||
|
||||
return [elementRef, isIntersecting];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
|
||||
if (media.matches !== matches) {
|
||||
setMatches(media.matches);
|
||||
}
|
||||
|
||||
const listener = (event: MediaQueryListEvent) => {
|
||||
setMatches(event.matches);
|
||||
};
|
||||
|
||||
media.addEventListener('change', listener);
|
||||
|
||||
return () => {
|
||||
media.removeEventListener('change', listener);
|
||||
};
|
||||
}, [matches, query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function useIsMobile(): boolean {
|
||||
return useMediaQuery('(max-width: 767px)');
|
||||
}
|
||||
|
||||
export function useIsTablet(): boolean {
|
||||
return useMediaQuery('(min-width: 768px) and (max-width: 1023px)');
|
||||
}
|
||||
|
||||
export function useIsDesktop(): boolean {
|
||||
return useMediaQuery('(min-width: 1024px)');
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface UseScrollRevealOptions {
|
||||
threshold?: number;
|
||||
rootMargin?: string;
|
||||
triggerOnce?: boolean;
|
||||
}
|
||||
|
||||
export function useScrollReveal(options: UseScrollRevealOptions = {}) {
|
||||
const { threshold = 0.1, rootMargin = '0px', triggerOnce = true } = options;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [isRevealed, setIsRevealed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsRevealed(true);
|
||||
if (triggerOnce) {
|
||||
observer.unobserve(element);
|
||||
}
|
||||
} else if (!triggerOnce) {
|
||||
setIsRevealed(false);
|
||||
}
|
||||
},
|
||||
{ threshold, rootMargin }
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [threshold, rootMargin, triggerOnce]);
|
||||
|
||||
return { ref, isRevealed };
|
||||
}
|
||||
|
||||
export function useCountUp(end: number, duration: number = 2000, start: number = 0) {
|
||||
const [count, setCount] = useState(start);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
const startAnimation = () => {
|
||||
if (isAnimating) return;
|
||||
setIsAnimating(true);
|
||||
|
||||
const startTime = Date.now();
|
||||
const diff = end - start;
|
||||
|
||||
const animate = () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||
const current = Math.floor(start + diff * easeOutQuart);
|
||||
|
||||
setCount(current);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
setIsAnimating(false);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
return { count, startAnimation, isAnimating };
|
||||
}
|
||||
|
||||
export function useStampAnimation() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !hasAnimated) {
|
||||
setHasAnimated(true);
|
||||
observer.unobserve(element);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [hasAnimated]);
|
||||
|
||||
return { ref, hasAnimated };
|
||||
}
|
||||
Reference in New Issue
Block a user