Populated Content
This commit is contained in:
239
resources/js/pages/Home.vue
Normal file
239
resources/js/pages/Home.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<Hero />
|
||||
<!-- About Section -->
|
||||
<About />
|
||||
<!-- Featured Tours Section -->
|
||||
<FeaturedTours />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import AppLayout from '../layouts/AppLayout.vue'
|
||||
import Hero from "@/components/home/Hero.vue";
|
||||
import About from "@/components/home/About.vue";
|
||||
import FeaturedTours from "@/components/home/FeaturedTours.vue";
|
||||
defineOptions({
|
||||
layout: AppLayout
|
||||
})
|
||||
|
||||
// Types
|
||||
interface IntersectionObserverOptions {
|
||||
threshold?: number;
|
||||
rootMargin?: string;
|
||||
}
|
||||
|
||||
// Intersection Observer for animations
|
||||
const createIntersectionObserver = (
|
||||
callback: IntersectionObserverCallback,
|
||||
options: IntersectionObserverOptions = {}
|
||||
): IntersectionObserver => {
|
||||
const defaultOptions: IntersectionObserverOptions = {
|
||||
threshold: 0.3,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
return new IntersectionObserver(callback, { ...defaultOptions, ...options });
|
||||
};
|
||||
|
||||
// About section animation
|
||||
const initAboutAnimation = (): void => {
|
||||
const aboutContent = document.querySelector('.about-content') as HTMLElement | null;
|
||||
if (!aboutContent) return;
|
||||
|
||||
const observer = createIntersectionObserver((entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry: IntersectionObserverEntry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
} else {
|
||||
entry.target.classList.remove('visible');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(aboutContent);
|
||||
};
|
||||
|
||||
// Featured tours animation
|
||||
const initToursAnimation = (): void => {
|
||||
const toursHeader = document.querySelector('.tours-header') as HTMLElement | null;
|
||||
const tourCards = document.querySelectorAll('.tour-card') as NodeListOf<HTMLElement>;
|
||||
|
||||
if (!toursHeader || !tourCards.length) return;
|
||||
|
||||
// Header animation
|
||||
const headerObserver = createIntersectionObserver((entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry: IntersectionObserverEntry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
headerObserver.observe(toursHeader);
|
||||
|
||||
// Tour cards animation with staggered effect
|
||||
const cardsObserver = createIntersectionObserver((entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry: IntersectionObserverEntry) => {
|
||||
const tourCard = entry.target as HTMLElement;
|
||||
const index = parseInt(tourCard.dataset.index || '0');
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
// Staggered animation when scrolling down
|
||||
setTimeout(() => {
|
||||
tourCard.classList.add('visible');
|
||||
}, index * 150);
|
||||
} else {
|
||||
// Reverse staggered animation when scrolling up
|
||||
const reverseIndex = (tourCards.length - 1) - index;
|
||||
setTimeout(() => {
|
||||
tourCard.classList.remove('visible');
|
||||
}, reverseIndex * 100);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.2 });
|
||||
|
||||
tourCards.forEach((card: HTMLElement) => {
|
||||
cardsObserver.observe(card);
|
||||
});
|
||||
};
|
||||
|
||||
// Smooth scrolling for navigation links
|
||||
const initSmoothScrolling = (): void => {
|
||||
const navLinks = document.querySelectorAll('a[href^="#"]') as NodeListOf<HTMLAnchorElement>;
|
||||
|
||||
navLinks.forEach((link: HTMLAnchorElement) => {
|
||||
link.addEventListener('click', (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = link.getAttribute('href');
|
||||
const targetElement = targetId ? document.querySelector(targetId) as HTMLElement | null : null;
|
||||
|
||||
if (targetElement) {
|
||||
const header = document.querySelector('.header') as HTMLElement | null;
|
||||
const headerHeight = header ? header.offsetHeight : 0;
|
||||
const targetPosition = targetElement.offsetTop - headerHeight;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// Close dropdown if open
|
||||
const dropdown = document.querySelector('.dropdown') as HTMLElement | null;
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('open');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Parallax effect for hero background
|
||||
const initParallax = (): void => {
|
||||
const heroBackground = document.querySelector('.hero-bg') as HTMLElement | null;
|
||||
const heroSection = document.querySelector('.hero') as HTMLElement | null;
|
||||
if (!heroBackground || !heroSection) return;
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrolled = window.pageYOffset;
|
||||
const heroRect = heroSection.getBoundingClientRect();
|
||||
const heroHeight = heroSection.offsetHeight;
|
||||
|
||||
if (heroRect.bottom > 0) {
|
||||
const rate = scrolled * -0.15;
|
||||
const maxMovement = heroHeight * 0.1;
|
||||
const constrainedRate = Math.max(rate, -maxMovement);
|
||||
|
||||
heroBackground.style.transform = `translateY(${constrainedRate}px)`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Add loading animations
|
||||
const initLoadingAnimations = (): void => {
|
||||
// Add class to trigger CSS animations
|
||||
document.body.classList.add('loaded');
|
||||
|
||||
// Animate hero content
|
||||
const heroTitle = document.querySelector('.hero-title') as HTMLElement | null;
|
||||
const heroSubtitle = document.querySelector('.hero-subtitle') as HTMLElement | null;
|
||||
|
||||
if (heroTitle) {
|
||||
setTimeout(() => {
|
||||
heroTitle.style.opacity = '1';
|
||||
heroTitle.style.transform = 'translateY(0)';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
if (heroSubtitle) {
|
||||
setTimeout(() => {
|
||||
heroSubtitle.style.opacity = '1';
|
||||
heroSubtitle.style.transform = 'translateY(0)';
|
||||
}, 600);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Tour card interactions
|
||||
const initTourCardInteractions = (): void => {
|
||||
const tourCards = document.querySelectorAll('.tour-card') as NodeListOf<HTMLElement>;
|
||||
|
||||
tourCards.forEach((card: HTMLElement) => {
|
||||
const bookButton = card.querySelector('.btn') as HTMLElement | null;
|
||||
|
||||
if (bookButton) {
|
||||
bookButton.addEventListener('click', () => {
|
||||
// Add your booking logic here
|
||||
alert('Booking functionality would be implemented here!');
|
||||
});
|
||||
}
|
||||
|
||||
// Add hover effects
|
||||
card.addEventListener('mouseenter', () => {
|
||||
card.style.transform = 'translateY(-8px) scale(1.02)';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', () => {
|
||||
if (card.classList.contains('visible')) {
|
||||
card.style.transform = 'translateY(0) rotate(0deg) scale(1)';
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize all functionality when DOM is loaded
|
||||
onMounted(() => {
|
||||
initAboutAnimation();
|
||||
initToursAnimation();
|
||||
initSmoothScrolling();
|
||||
initParallax();
|
||||
initLoadingAnimations();
|
||||
initTourCardInteractions();
|
||||
});
|
||||
|
||||
// Add performance optimization
|
||||
const debounce = <T extends (...args: any[]) => void>(func: T, wait: number): T => {
|
||||
let timeout: number | undefined;
|
||||
return ((...args: Parameters<T>) => {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
}) as T;
|
||||
};
|
||||
|
||||
// Optimize scroll events
|
||||
window.addEventListener('scroll', debounce(() => {
|
||||
// Scroll-based animations can be added here
|
||||
}, 16)); // ~60fps
|
||||
</script>
|
||||
Reference in New Issue
Block a user