- Create useIntersectionObserver composable for scroll-triggered animations - Add TimelineItem component with alternating layout (desktop) - Implement journey page with i18n-based milestones data - Add 7 career milestones (2018-2025) in FR and EN - Gradient timeline line with animated connection points - Glassmorphism card design with hover effects - Respect prefers-reduced-motion for all animations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
58 lines
1.2 KiB
TypeScript
58 lines
1.2 KiB
TypeScript
export interface UseIntersectionObserverOptions {
|
|
threshold?: number
|
|
rootMargin?: string
|
|
once?: boolean
|
|
}
|
|
|
|
export function useIntersectionObserver(
|
|
target: Ref<HTMLElement | null>,
|
|
options: UseIntersectionObserverOptions = {}
|
|
) {
|
|
const { threshold = 0.1, rootMargin = '0px', once = true } = options
|
|
|
|
const isVisible = ref(false)
|
|
|
|
if (import.meta.client) {
|
|
let observer: IntersectionObserver | null = null
|
|
|
|
const cleanup = () => {
|
|
if (observer) {
|
|
observer.disconnect()
|
|
observer = null
|
|
}
|
|
}
|
|
|
|
watch(
|
|
target,
|
|
(el) => {
|
|
cleanup()
|
|
|
|
if (!el) return
|
|
|
|
observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
isVisible.value = true
|
|
if (once && observer) {
|
|
observer.unobserve(entry.target)
|
|
}
|
|
} else if (!once) {
|
|
isVisible.value = false
|
|
}
|
|
})
|
|
},
|
|
{ threshold, rootMargin }
|
|
)
|
|
|
|
observer.observe(el)
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
onUnmounted(cleanup)
|
|
}
|
|
|
|
return { isVisible: readonly(isVisible) }
|
|
}
|