Componentalization

This commit is contained in:
2025-09-16 22:01:42 +10:00
parent e965cc2780
commit 91771e9573
19 changed files with 1683 additions and 223 deletions

View File

@@ -10,7 +10,7 @@
</nav>
<div class="nav-menu">
<a href="#about" class="nav-link">About</a>
<a href="/about" class="nav-link">About</a>
<a href="#contact" class="nav-link">Contact</a>
<div
class="dropdown"
@@ -32,7 +32,7 @@
<div class="dropdown-menu" :class="{ 'is-visible': isDropdownVisible }">
<a
v-for="continent in sortedContinents"
v-for="continent in continents_with_tours"
:key="continent.id"
:href="`/tours/${continent.internal_name}`"
>
@@ -48,15 +48,10 @@
<script setup lang="ts">
import { onMounted, onUnmounted, computed, ref } from 'vue';
import { usePage} from "@inertiajs/vue3";
import {Continent, GlobalProperties} from "@/types";
import {GlobalProperties} from "@/types";
const { continents_with_tours } = usePage().props as unknown as GlobalProperties
const sortedContinents = computed(() => {
return [...continents_with_tours].sort((a, b) => a.name.localeCompare(b.name))
})
const dropdownOpen = ref(false)
const windowWidth = ref(window.innerWidth);
@@ -128,7 +123,6 @@ const initDropdown = (): void => {
}
});
// Close dropdown when pressing escape
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
dropdown.classList.remove('open');

View File

@@ -26,9 +26,9 @@ const props = defineProps<{
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
{{ tour.countries.at(0)?.name }}, {{ tour.countries.at(0)?.continent.name }}
{{ tour.countries?.at(0)?.name }}
</div>
<h3 class="tour-title">{{tour.title}}</h3>
<h3 class="tour-title">{{tour.name}}</h3>
<p class="tour-description" v-if="tour.short_description">{{tour.short_description}}</p>
<div class="tour-details">
<div class="tour-detail">

View File

@@ -28,7 +28,7 @@
<div class="about-cta">
<h3>Who Are We?</h3>
<p>Dr Edgy was founded by two close friends who met travelling in North Korea. Both of us love adventure travel with a bit of luxury. One of us is even a doctor.</p>
<p>Dr Edgy was founded by two close friends who met travelling in North Korea. Both of us love adventure travel with a bit of luxury. One of us is even an actual doctor.</p>
<p> You are in safe hands.</p>
<EdgyButton classes="btn-primary">GET TO KNOW US</EdgyButton>
</div>
@@ -178,9 +178,32 @@
<script setup lang="ts">
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
import GradientText from "@/components/dredgy/GradientText.vue";
import SectionContainer from "@/components/dredgy/SectionContainer.vue";
import SectionTitle from "@/components/dredgy/SectionTitle.vue";
import {createIntersectionObserver} from "@/composables/useIntersectionObserver";
import {onMounted} from "vue";
onMounted(() => {
initAboutAnimation()
})
// 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);
};
</script>

View File

@@ -12,18 +12,22 @@
<div class="tours-grid">
<TourCard
v-for="tour in featuredTours"
v-for="(tour, index) in featuredTours"
:key="tour.id ?? index"
class="tour-card"
:data-index="index"
:tour="tour"
/>
</div>
<br/>
<EdgyButton classes="btn-primary btn-full">Explore All Tours</EdgyButton>
<EdgyButton classes="btn-primary btn-full">Explore All Adventures</EdgyButton>
</SectionContainer>
</section>
</template>
<style scoped>
/* Featured Tours Section */
.featured-tours {
@@ -99,12 +103,77 @@ import SectionTitle from "@/components/dredgy/SectionTitle.vue";
import TourCard from "@/components/dredgy/TourCard.vue";
import { Tour } from "@/types";
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
const featuredTours : Tour[] = [];
/*
import { defineProps } from 'vue'
import { defineProps, onMounted } from 'vue'
import {createIntersectionObserver} from "@/composables/useIntersectionObserver";
const props = defineProps<{
featuredTours: Tour[]
}>()*/
}>()
onMounted(() => {
initToursAnimation()
initTourCardInteractions()
})
const initTourCardInteractions = (): void => {
const tourCards = document.querySelectorAll('.tour-card') as NodeListOf<HTMLElement>;
tourCards.forEach((card: HTMLElement) => {
// 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)';
}
});
});
};
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);
});
};
</script>

View File

@@ -14,7 +14,7 @@
We set the trends.
</p>
<div class="hero-buttons">
<EdgyButton classes="btn-secondary">View Tours</EdgyButton>
<EdgyButton classes="btn-secondary">View Adventures</EdgyButton>
</div>
</div>
@@ -91,6 +91,22 @@
align-items: center;
}
.hero-title,
.hero-subtitle {
opacity: 0;
transform: translateY(18px);
transition: opacity 1000ms cubic-bezier(.22,.9,.32,1),
transform 1000ms cubic-bezier(.22,.9,.32,1);
will-change: opacity, transform;
}
/* visible state toggled by JS */
.hero-title.is-visible,
.hero-subtitle.is-visible {
opacity: 1;
transform: translateY(0);
}
@media (min-width: 640px) {
.hero-buttons {
flex-direction: row;
@@ -104,6 +120,31 @@
}
}
/* Animations */
@keyframes bounce {
0%, 20%, 53%, 80%, 100% {
transform: translateX(-50%) translateY(0);
}
40%, 43% {
transform: translateX(-50%) translateY(-15px);
}
70% {
transform: translateX(-50%) translateY(-7px);
}
90% {
transform: translateX(-50%) translateY(-2px);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.scroll-indicator {
position: absolute;
bottom: 2rem;
@@ -136,4 +177,54 @@
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
import GradientText from "@/components/dredgy/GradientText.vue";
import {onMounted} from "vue";
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 => {
document.body.classList.add('loaded');
const heroTitle = document.querySelector('.hero-title') as HTMLElement | null;
const heroSubtitle = document.querySelector('.hero-subtitle') as HTMLElement | null;
if (heroTitle) {
setTimeout(() => {
// force reflow (safe) then add class to trigger transition
void heroTitle.offsetWidth;
heroTitle.classList.add('is-visible');
}, 500);
}
if (heroSubtitle) {
setTimeout(() => {
void heroSubtitle.offsetWidth;
heroSubtitle.classList.add('is-visible');
}, 750);
}
};
onMounted(() => {
initParallax()
initLoadingAnimations()
})
</script>